diff options
Diffstat (limited to 'lib/api')
54 files changed, 1592 insertions, 973 deletions
diff --git a/lib/api/api.rb b/lib/api/api.rb index bf8ddba6f0d..f4a96b9711b 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -6,7 +6,7 @@ module API LOG_FILENAME = Rails.root.join("log", "api_json.log") - NO_SLASH_URL_PART_REGEX = %r{[^/]+} + NO_SLASH_URL_PART_REGEX = %r{[^/]+}.freeze NAMESPACE_OR_PROJECT_REQUIREMENTS = { id: NO_SLASH_URL_PART_REGEX }.freeze COMMIT_ENDPOINT_REQUIREMENTS = NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(sha: NO_SLASH_URL_PART_REGEX).freeze USER_REQUIREMENTS = { user_id: NO_SLASH_URL_PART_REGEX }.freeze @@ -134,6 +134,7 @@ module API mount ::API::Pipelines mount ::API::PipelineSchedules mount ::API::ProjectClusters + mount ::API::ProjectEvents mount ::API::ProjectExport mount ::API::ProjectImport mount ::API::ProjectHooks diff --git a/lib/api/branches.rb b/lib/api/branches.rb index 07f529b01bb..65d7f68bbf9 100644 --- a/lib/api/branches.rb +++ b/lib/api/branches.rb @@ -34,11 +34,11 @@ module API repository = user_project.repository branches = BranchesFinder.new(repository, declared_params(include_missing: false)).execute - branches = ::Kaminari.paginate_array(branches) + branches = paginate(::Kaminari.paginate_array(branches)) merged_branch_names = repository.merged_branch_names(branches.map(&:name)) present( - paginate(branches), + branches, with: Entities::Branch, current_user: current_user, project: user_project, @@ -162,8 +162,8 @@ module API result = DeleteBranchService.new(user_project, current_user) .execute(params[:branch]) - if result[:status] != :success - render_api_error!(result[:message], result[:return_code]) + if result.error? + render_api_error!(result.message, result.http_status) end end end diff --git a/lib/api/commits.rb b/lib/api/commits.rb index 8defc59224d..65eb9bfb87e 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -99,6 +99,7 @@ module API optional :author_email, type: String, desc: 'Author email for commit' optional :author_name, type: String, desc: 'Author name for commit' optional :stats, type: Boolean, default: true, desc: 'Include commit stats' + optional :force, type: Boolean, default: false, desc: 'When `true` overwrites the target branch with a new commit based on the `start_branch`' end post ':id/repository/commits' do authorize_push_to_branch!(params[:branch]) @@ -318,10 +319,18 @@ module API use :pagination end get ':id/repository/commits/:sha/merge_requests', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do + authorize! :read_merge_request, user_project + commit = user_project.commit(params[:sha]) not_found! 'Commit' unless commit - present paginate(commit.merge_requests), with: Entities::MergeRequestBasic + commit_merge_requests = MergeRequestsFinder.new( + current_user, + project_id: user_project.id, + commit_sha: commit.sha + ).execute + + present paginate(commit_merge_requests), with: Entities::MergeRequestBasic end desc "Get a commit's GPG signature" do diff --git a/lib/api/discussions.rb b/lib/api/discussions.rb index 91eb6a23701..5928ee1657b 100644 --- a/lib/api/discussions.rb +++ b/lib/api/discussions.rb @@ -7,9 +7,7 @@ module API before { authenticate! } - NOTEABLE_TYPES = [Issue, Snippet, MergeRequest, Commit].freeze - - NOTEABLE_TYPES.each do |noteable_type| + Helpers::DiscussionsHelpers.noteable_types.each do |noteable_type| parent_type = noteable_type.parent_class.to_s.underscore noteables_str = noteable_type.to_s.underscore.pluralize noteables_path = noteable_type == Commit ? "repository/#{noteables_str}" : noteables_str @@ -136,9 +134,13 @@ module API post ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id/notes" do noteable = find_noteable(parent_type, noteables_str, params[:noteable_id]) notes = readable_discussion_notes(noteable, params[:discussion_id]) + first_note = notes.first break not_found!("Discussion") if notes.empty? - break bad_request!("Discussion is an individual note.") unless notes.first.part_of_discussion? + + unless first_note.part_of_discussion? || first_note.to_discussion.can_convert_to_discussion? + break bad_request!("Discussion can not be replied to.") + end opts = { note: params[:body], diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 18f15632f2b..296688ba25b 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -86,6 +86,10 @@ module API expose :admin?, as: :is_admin end + class UserDetailsWithAdmin < UserWithAdmin + expose :highest_role + end + class UserStatus < Grape::Entity expose :emoji expose :message @@ -156,7 +160,7 @@ module API class BasicProjectDetails < ProjectIdentity include ::API::ProjectsRelationBuilder - expose :default_branch + expose :default_branch, if: -> (project, options) { Ability.allowed?(options[:current_user], :download_code, project) } # Avoids an N+1 query: https://github.com/mbleigh/acts-as-taggable-on/issues/91#issuecomment-168273770 expose :tag_list do |project| # project.tags.order(:name).pluck(:name) is the most suitable option @@ -261,7 +265,7 @@ module API expose :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[:current_user]) } expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] } expose :public_builds, as: :public_jobs - expose :ci_config_path + expose :ci_config_path, if: -> (project, options) { Ability.allowed?(options[:current_user], :download_code, project) } expose :shared_with_groups do |project, options| SharedGroup.represent(project.project_group_links, options) end @@ -270,8 +274,10 @@ module API expose :only_allow_merge_if_all_discussions_are_resolved expose :printing_merge_request_link_enabled expose :merge_method - - expose :statistics, using: 'API::Entities::ProjectStatistics', if: :statistics + expose :statistics, using: 'API::Entities::ProjectStatistics', if: -> (project, options) { + options[:statistics] && Ability.allowed?(options[:current_user], :read_statistics, project) + } + expose :external_authorization_classification_label # rubocop: disable CodeReuse/ActiveRecord def self.preload_relation(projects_relation, options = {}) @@ -658,7 +664,11 @@ module API expose(:user_notes_count) { |merge_request, options| issuable_metadata(merge_request, options, :user_notes_count) } expose(:upvotes) { |merge_request, options| issuable_metadata(merge_request, options, :upvotes) } expose(:downvotes) { |merge_request, options| issuable_metadata(merge_request, options, :downvotes) } - expose :author, :assignee, using: Entities::UserBasic + expose :assignee, using: ::API::Entities::UserBasic do |merge_request| + merge_request.assignee + end + expose :author, :assignees, using: Entities::UserBasic + expose :source_project_id, :target_project_id expose :labels do |merge_request| # Avoids an N+1 query since labels are preloaded @@ -685,6 +695,10 @@ module API # Deprecated expose :allow_collaboration, as: :allow_maintainer_to_push, if: -> (merge_request, _) { merge_request.for_fork? } + expose :reference do |merge_request, options| + merge_request.to_reference(options[:project]) + end + expose :web_url do |merge_request| Gitlab::UrlBuilder.build(merge_request) end @@ -721,6 +735,8 @@ module API merge_request.metrics&.pipeline end + expose :head_pipeline, using: 'API::Entities::Pipeline' + expose :diff_refs, using: Entities::DiffRefs # Allow the status of a rebase to be determined @@ -862,7 +878,7 @@ module API expose :push_event_payload, as: :push_data, using: PushEventPayload, - if: -> (event, _) { event.push? } + if: -> (event, _) { event.push_action? } expose :author_username do |event, options| event.author&.username @@ -882,7 +898,8 @@ module API expose :target_type expose :target do |todo, options| - todo_target_class(todo.target_type).represent(todo.target, options) + todo_options = options.fetch(todo.target_type, {}) + todo_target_class(todo.target_type).represent(todo.target, todo_options) end expose :target_url do |todo, options| @@ -905,7 +922,15 @@ module API end class NamespaceBasic < Grape::Entity - expose :id, :name, :path, :kind, :full_path, :parent_id + expose :id, :name, :path, :kind, :full_path, :parent_id, :avatar_url + + expose :web_url do |namespace| + if namespace.user? + Gitlab::Routing.url_helpers.user_url(namespace.owner) + else + namespace.web_url + end + end end class Namespace < NamespaceBasic @@ -1104,6 +1129,8 @@ module API expose(:default_snippet_visibility) { |setting, _options| Gitlab::VisibilityLevel.string_level(setting.default_snippet_visibility) } expose(:default_group_visibility) { |setting, _options| Gitlab::VisibilityLevel.string_level(setting.default_group_visibility) } + expose(*::ApplicationSettingsHelper.external_authorization_service_attributes) + # support legacy names, can be removed in v5 expose :password_authentication_enabled_for_web, as: :password_authentication_enabled expose :password_authentication_enabled_for_web, as: :signin_enabled @@ -1129,22 +1156,33 @@ module API end end - class Release < TagRelease + class Release < Grape::Entity expose :name + expose :tag, as: :tag_name, if: lambda { |_, _| can_download_code? } + expose :description expose :description_html do |entity| MarkupHelper.markdown_field(entity, :description) end expose :created_at expose :author, using: Entities::UserBasic, if: -> (release, _) { release.author.present? } - expose :commit, using: Entities::Commit + expose :commit, using: Entities::Commit, if: lambda { |_, _| can_download_code? } expose :assets do - expose :assets_count, as: :count - expose :sources, using: Entities::Releases::Source + expose :assets_count, as: :count do |release, _| + assets_to_exclude = can_download_code? ? [] : [:sources] + release.assets_count(except: assets_to_exclude) + end + expose :sources, using: Entities::Releases::Source, if: lambda { |_, _| can_download_code? } expose :links, using: Entities::Releases::Link do |release, options| release.links.sorted end end + + private + + def can_download_code? + Ability.allowed?(options[:current_user], :download_code, object.project) + end end class Tag < Grape::Entity @@ -1250,7 +1288,7 @@ module API end class Variable < Grape::Entity - expose :key, :value + expose :variable_type, :key, :value expose :protected?, as: :protected, if: -> (entity, _) { entity.respond_to?(:protected?) } end @@ -1261,6 +1299,9 @@ module API expose :created_at, :updated_at, :started_at, :finished_at, :committed_at expose :duration expose :coverage + expose :detailed_status, using: DetailedStatusEntity do |pipeline, options| + pipeline.detailed_status(options[:current_user]) + end end class PipelineSchedule < Grape::Entity @@ -1279,10 +1320,6 @@ module API expose :id, :name, :slug, :external_url end - class Environment < EnvironmentBasic - expose :project, using: Entities::BasicProjectDetails - end - class Deployment < Grape::Entity expose :id, :iid, :ref, :sha, :created_at expose :user, using: Entities::UserBasic @@ -1290,6 +1327,11 @@ module API expose :deployable, using: Entities::Job end + class Environment < EnvironmentBasic + expose :project, using: Entities::BasicProjectDetails + expose :last_deployment, using: Entities::Deployment, if: { last_deployment: true } + end + class LicenseBasic < Grape::Entity expose :key, :name, :nickname expose :url, as: :html_url @@ -1383,8 +1425,13 @@ module API expose :name, :script, :timeout, :when, :allow_failure end + class Port < Grape::Entity + expose :number, :protocol, :name + end + class Image < Grape::Entity expose :name, :entrypoint + expose :ports, using: JobRequest::Port end class Service < Image @@ -1552,8 +1599,6 @@ module API class Suggestion < Grape::Entity expose :id - expose :from_original_line - expose :to_original_line expose :from_line expose :to_line expose :appliable?, as: :appliable @@ -1584,7 +1629,7 @@ module API end class Cluster < Grape::Entity - expose :id, :name, :created_at + expose :id, :name, :created_at, :domain expose :provider_type, :platform_type, :environment_scope, :cluster_type expose :user, using: Entities::UserBasic expose :platform_kubernetes, using: Entities::Platform::Kubernetes diff --git a/lib/api/environments.rb b/lib/api/environments.rb index 0278c6c54a5..6cd43923559 100644 --- a/lib/api/environments.rb +++ b/lib/api/environments.rb @@ -22,7 +22,7 @@ module API get ':id/environments' do authorize! :read_environment, user_project - present paginate(user_project.environments), with: Entities::Environment + present paginate(user_project.environments), with: Entities::Environment, current_user: current_user end desc 'Creates a new environment' do @@ -40,7 +40,7 @@ module API environment = user_project.environments.create(declared_params) if environment.persisted? - present environment, with: Entities::Environment + present environment, with: Entities::Environment, current_user: current_user else render_validation_error!(environment) end @@ -63,7 +63,7 @@ module API update_params = declared_params(include_missing: false).extract!(:name, :external_url) if environment.update(update_params) - present environment, with: Entities::Environment + present environment, with: Entities::Environment, current_user: current_user else render_validation_error!(environment) end @@ -99,7 +99,22 @@ module API environment.stop_with_action!(current_user) status 200 - present environment, with: Entities::Environment + present environment, with: Entities::Environment, current_user: current_user + end + + desc 'Get a single environment' do + success Entities::Environment + end + params do + requires :environment_id, type: Integer, desc: 'The environment ID' + end + get ':id/environments/:environment_id' do + authorize! :read_environment, user_project + + environment = user_project.environments.find(params[:environment_id]) + present environment, with: Entities::Environment, current_user: current_user, + except: [:project, { last_deployment: [:environment] }], + last_deployment: true end end end diff --git a/lib/api/events.rb b/lib/api/events.rb index b98aa9f31e1..e4c017fab42 100644 --- a/lib/api/events.rb +++ b/lib/api/events.rb @@ -4,34 +4,11 @@ module API class Events < Grape::API include PaginationParams include APIGuard + helpers ::API::Helpers::EventsHelpers - helpers do - params :event_filter_params do - optional :action, type: String, values: Event.actions, desc: 'Event action to filter on' - optional :target_type, type: String, values: Event.target_types, desc: 'Event target type to filter on' - optional :before, type: Date, desc: 'Include only events created before this date' - optional :after, type: Date, desc: 'Include only events created after this date' - end - - params :sort_params do - optional :sort, type: String, values: %w[asc desc], default: 'desc', - desc: 'Return events sorted in ascending and descending order' - end - - def present_events(events) - events = paginate(events) - - present events, with: Entities::Event - end - - def find_events(source) - EventsFinder.new(params.merge(source: source, current_user: current_user, with_associations: true)).execute - end - end + allow_access_with_scope :read_user, if: -> (request) { request.get? } resource :events do - allow_access_with_scope :read_user, if: -> (request) { request.get? } - desc "List currently authenticated user's events" do detail 'This feature was introduced in GitLab 9.3.' success Entities::Event @@ -55,8 +32,6 @@ module API requires :id, type: String, desc: 'The ID or Username of the user' end resource :users do - allow_access_with_scope :read_user, if: -> (request) { request.get? } - desc 'Get the contribution events of a specified user' do detail 'This feature was introduced in GitLab 8.13.' success Entities::Event @@ -76,25 +51,5 @@ module API present_events(events) end end - - params do - requires :id, type: String, desc: 'The ID of a project' - end - resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do - desc "List a Project's visible events" do - success Entities::Event - end - params do - use :pagination - use :event_filter_params - use :sort_params - end - - get ":id/events" do - events = find_events(user_project) - - present_events(events) - end - end end end diff --git a/lib/api/group_variables.rb b/lib/api/group_variables.rb index 3f048e0dc56..47fcbabb4d4 100644 --- a/lib/api/group_variables.rb +++ b/lib/api/group_variables.rb @@ -47,6 +47,7 @@ module API requires :key, type: String, desc: 'The key of the variable' requires :value, type: String, desc: 'The value of the variable' optional :protected, type: String, desc: 'Whether the variable is protected' + optional :variable_type, type: String, values: Ci::GroupVariable.variable_types.keys, desc: 'The type of variable, must be one of env_var or file. Defaults to env_var' end post ':id/variables' do variable_params = declared_params(include_missing: false) @@ -67,6 +68,7 @@ module API optional :key, type: String, desc: 'The key of the variable' optional :value, type: String, desc: 'The value of the variable' optional :protected, type: String, desc: 'Whether the variable is protected' + optional :variable_type, type: String, values: Ci::GroupVariable.variable_types.keys, desc: 'The type of variable, must be one of env_var or file' end # rubocop: disable CodeReuse/ActiveRecord put ':id/variables/:key' do diff --git a/lib/api/groups.rb b/lib/api/groups.rb index 64958ff982a..ad16f26f5cc 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -20,8 +20,20 @@ module API optional :share_with_group_lock, type: Boolean, desc: 'Prevent sharing a project with another group within this group' end + if Gitlab.ee? + params :optional_params_ee do + optional :membership_lock, type: Boolean, desc: 'Prevent adding new members to project membership within this group' + optional :ldap_cn, type: String, desc: 'LDAP Common Name' + optional :ldap_access, type: Integer, desc: 'A valid access level' + optional :shared_runners_minutes_limit, type: Integer, desc: '(admin-only) Pipeline minutes quota for this group' + optional :extra_shared_runners_minutes_limit, type: Integer, desc: '(admin-only) Extra pipeline minutes quota for this group' + all_or_none_of :ldap_cn, :ldap_access + end + end + params :optional_params do use :optional_params_ce + use :optional_params_ee if Gitlab.ee? end params :statistics_params do @@ -58,6 +70,22 @@ module API end # rubocop: enable CodeReuse/ActiveRecord + def create_group + # This is a separate method so that EE can extend its behaviour, without + # having to modify this code directly. + ::Groups::CreateService + .new(current_user, declared_params(include_missing: false)) + .execute + end + + def update_group(group) + # This is a separate method so that EE can extend its behaviour, without + # having to modify this code directly. + ::Groups::UpdateService + .new(group, current_user, declared_params(include_missing: false)) + .execute + end + def find_group_projects(params) group = find_group!(params[:id]) options = { @@ -127,7 +155,7 @@ module API authorize! :create_group end - group = ::Groups::CreateService.new(current_user, declared_params(include_missing: false)).execute + group = create_group if group.persisted? present group, with: Entities::GroupDetail, current_user: current_user @@ -148,12 +176,16 @@ module API optional :name, type: String, desc: 'The name of the group' optional :path, type: String, desc: 'The path of the group' use :optional_params + + if Gitlab.ee? + optional :file_template_project_id, type: Integer, desc: 'The ID of a project to use for custom templates in this group' + end end put ':id' do group = find_group!(params[:id]) authorize! :admin_group, group - if ::Groups::UpdateService.new(group, current_user, declared_params(include_missing: false)).execute + if update_group(group) present group, with: Entities::GroupDetail, current_user: current_user else render_validation_error!(group) diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 54cd4cd9cdb..7e4539d0419 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -6,6 +6,7 @@ module API include Helpers::Pagination SUDO_HEADER = "HTTP_SUDO".freeze + GITLAB_SHARED_SECRET_HEADER = "Gitlab-Shared-Secret".freeze SUDO_PARAM = :sudo API_USER_ENV = 'gitlab.api.user'.freeze @@ -66,10 +67,6 @@ module API initial_current_user != current_user end - def user_namespace - @user_namespace ||= find_namespace!(params[:id]) - end - def user_group @group ||= find_group!(params[:id]) end @@ -212,10 +209,12 @@ module API end def authenticate_by_gitlab_shell_token! - input = params['secret_token'].try(:chomp) - unless Devise.secure_compare(secret_token, input) - unauthorized! - end + input = params['secret_token'] + input ||= Base64.decode64(headers[GITLAB_SHARED_SECRET_HEADER]) if headers.key?(GITLAB_SHARED_SECRET_HEADER) + + input&.chomp! + + unauthorized! unless Devise.secure_compare(secret_token, input) end def authenticated_with_full_private_access! @@ -244,6 +243,10 @@ module API authorize! :read_build, user_project end + def authorize_destroy_artifacts! + authorize! :destroy_artifacts, user_project + end + def authorize_update_builds! authorize! :update_build, user_project end @@ -295,6 +298,12 @@ module API end # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord + def filter_by_title(items, title) + items.where(title: title) + end + # rubocop: enable CodeReuse/ActiveRecord + def filter_by_search(items, text) items.search(text) end diff --git a/lib/api/helpers/custom_validators.rb b/lib/api/helpers/custom_validators.rb index 1058f4e8a5e..c86eae6f2da 100644 --- a/lib/api/helpers/custom_validators.rb +++ b/lib/api/helpers/custom_validators.rb @@ -22,9 +22,22 @@ module API message: "should be an integer, 'None' or 'Any'" end end + + class ArrayNoneAny < Grape::Validations::Base + def validate_param!(attr_name, params) + value = params[attr_name] + + return if value.is_a?(Array) || + [IssuableFinder::FILTER_NONE, IssuableFinder::FILTER_ANY].include?(value.to_s.downcase) + + raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], + message: "should be an array, 'None' or 'Any'" + end + end end end end Grape::Validations.register_validator(:absence, ::API::Helpers::CustomValidators::Absence) Grape::Validations.register_validator(:integer_none_any, ::API::Helpers::CustomValidators::IntegerNoneAny) +Grape::Validations.register_validator(:array_none_any, ::API::Helpers::CustomValidators::ArrayNoneAny) diff --git a/lib/api/helpers/discussions_helpers.rb b/lib/api/helpers/discussions_helpers.rb new file mode 100644 index 00000000000..94a5bf75c39 --- /dev/null +++ b/lib/api/helpers/discussions_helpers.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module API + module Helpers + module DiscussionsHelpers + def self.noteable_types + # This is a method instead of a constant, allowing EE to more easily + # extend it. + [Issue, Snippet, MergeRequest, Commit] + end + end + end +end diff --git a/lib/api/helpers/events_helpers.rb b/lib/api/helpers/events_helpers.rb new file mode 100644 index 00000000000..bf3b76bb92d --- /dev/null +++ b/lib/api/helpers/events_helpers.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module API + module Helpers + module EventsHelpers + extend Grape::API::Helpers + + params :event_filter_params do + optional :action, type: String, values: Event.actions, desc: 'Event action to filter on' + optional :target_type, type: String, values: Event.target_types, desc: 'Event target type to filter on' + optional :before, type: Date, desc: 'Include only events created before this date' + optional :after, type: Date, desc: 'Include only events created after this date' + end + + params :sort_params do + optional :sort, type: String, values: %w[asc desc], default: 'desc', + desc: 'Return events sorted in ascending and descending order' + end + + def present_events(events) + events = paginate(events) + + present events, with: Entities::Event + end + + def find_events(source) + EventsFinder.new(params.merge(source: source, current_user: current_user, with_associations: true)).execute + end + end + end +end diff --git a/lib/api/helpers/graphql_helpers.rb b/lib/api/helpers/graphql_helpers.rb new file mode 100644 index 00000000000..94010ab1bc2 --- /dev/null +++ b/lib/api/helpers/graphql_helpers.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module API + module Helpers + # GraphqlHelpers is used by the REST API when it is acting like a client + # against the graphql API. Helper code for the graphql server implementation + # should be in app/graphql/ or lib/gitlab/graphql/ + module GraphqlHelpers + def conditionally_graphql!(fallback:, query:, context: {}, transform: nil) + return fallback.call unless Feature.enabled?(:graphql) + + result = GitlabSchema.execute(query, context: context) + + if transform + transform.call(result) + else + result + end + end + end + end +end diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb index fe78049af87..71c30ec99a5 100644 --- a/lib/api/helpers/internal_helpers.rb +++ b/lib/api/helpers/internal_helpers.rb @@ -5,9 +5,11 @@ module API module InternalHelpers attr_reader :redirected_path - def wiki? - set_project unless defined?(@wiki) # rubocop:disable Gitlab/ModuleWithInstanceVariables - @wiki # rubocop:disable Gitlab/ModuleWithInstanceVariables + delegate :wiki?, to: :repo_type + + def repo_type + set_project unless defined?(@repo_type) # rubocop:disable Gitlab/ModuleWithInstanceVariables + @repo_type # rubocop:disable Gitlab/ModuleWithInstanceVariables end def project @@ -41,6 +43,28 @@ module API ::MergeRequests::GetUrlsService.new(project).execute(params[:changes]) end + def process_mr_push_options(push_options, project, user, changes) + output = {} + + service = ::MergeRequests::PushOptionsHandlerService.new( + project, + user, + changes, + push_options + ).execute + + if service.errors.present? + output[:warnings] = push_options_warning(service.errors.join("\n\n")) + end + + output + end + + def push_options_warning(warning) + options = Array.wrap(params[:push_options]).map { |p| "'#{p}'" }.join(' ') + "Error encountered with push options #{options}: #{warning}" + end + def redis_ping result = Gitlab::Redis::SharedState.with { |redis| redis.ping } @@ -67,10 +91,10 @@ module API # rubocop:disable Gitlab/ModuleWithInstanceVariables def set_project if params[:gl_repository] - @project, @wiki = Gitlab::GlRepository.parse(params[:gl_repository]) + @project, @repo_type = Gitlab::GlRepository.parse(params[:gl_repository]) @redirected_path = nil else - @project, @wiki, @redirected_path = Gitlab::RepoPath.parse(params[:project]) + @project, @repo_type, @redirected_path = Gitlab::RepoPath.parse(params[:project]) end end # rubocop:enable Gitlab/ModuleWithInstanceVariables @@ -78,7 +102,7 @@ module API # Project id to pass between components that don't share/don't have # access to the same filesystem mounts def gl_repository - Gitlab::GlRepository.gl_repository(project, wiki?) + repo_type.identifier_for_subject(project) end def gl_project_path @@ -92,7 +116,7 @@ module API # Return the repository depending on whether we want the wiki or the # regular repository def repository - if wiki? + if repo_type.wiki? project.wiki.repository else project.repository diff --git a/lib/api/helpers/issues_helpers.rb b/lib/api/helpers/issues_helpers.rb new file mode 100644 index 00000000000..f6762910b0c --- /dev/null +++ b/lib/api/helpers/issues_helpers.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module API + module Helpers + module IssuesHelpers + def self.update_params_at_least_one_of + [ + :assignee_id, + :assignee_ids, + :confidential, + :created_at, + :description, + :discussion_locked, + :due_date, + :labels, + :milestone_id, + :state_event, + :title + ] + end + end + end +end diff --git a/lib/api/helpers/notes_helpers.rb b/lib/api/helpers/notes_helpers.rb index 216b2c45741..a068de4361c 100644 --- a/lib/api/helpers/notes_helpers.rb +++ b/lib/api/helpers/notes_helpers.rb @@ -3,6 +3,12 @@ module API module Helpers module NotesHelpers + def self.noteable_types + # This is a method instead of a constant, allowing EE to more easily + # extend it. + [Issue, MergeRequest, Snippet] + end + def update_note(noteable, note_id) note = noteable.notes.find(params[:note_id]) @@ -70,14 +76,7 @@ module API def find_noteable(parent, noteables_str, noteable_id) noteable = public_send("find_#{parent}_#{noteables_str.singularize}", noteable_id) # rubocop:disable GitlabSecurity/PublicSend - readable = - if noteable.is_a?(Commit) - # for commits there is not :read_commit policy, check if user - # has :read_note permission on the commit's project - can?(current_user, :read_note, user_project) - else - can?(current_user, noteable_read_ability_name(noteable), noteable) - end + readable = can?(current_user, noteable_read_ability_name(noteable), noteable) return not_found!(noteables_str) unless readable @@ -89,12 +88,11 @@ module API end def create_note(noteable, opts) - policy_object = noteable.is_a?(Commit) ? user_project : noteable - authorize!(:create_note, policy_object) + authorize!(:create_note, noteable) parent = noteable_parent(noteable) - opts.delete(:created_at) unless current_user.can?(:set_note_created_at, policy_object) + opts.delete(:created_at) unless current_user.can?(:set_note_created_at, noteable) opts[:updated_at] = opts[:created_at] if opts[:created_at] diff --git a/lib/api/helpers/pagination.rb b/lib/api/helpers/pagination.rb index d00e61678b5..94b58a64d26 100644 --- a/lib/api/helpers/pagination.rb +++ b/lib/api/helpers/pagination.rb @@ -149,7 +149,7 @@ module API def conditions(pagination) fields = pagination.fields - return nil if fields.empty? + return if fields.empty? placeholder = fields.map { '?' } diff --git a/lib/api/helpers/projects_helpers.rb b/lib/api/helpers/projects_helpers.rb index e6a72b949f9..aaf32dafca4 100644 --- a/lib/api/helpers/projects_helpers.rb +++ b/lib/api/helpers/projects_helpers.rb @@ -29,13 +29,53 @@ module API optional :printing_merge_request_link_enabled, type: Boolean, desc: 'Show link to create/view merge request when pushing from the command line' optional :merge_method, type: String, values: %w(ff rebase_merge merge), desc: 'The merge method used when merging merge requests' optional :initialize_with_readme, type: Boolean, desc: "Initialize a project with a README.md" + optional :external_authorization_classification_label, type: String, desc: 'The classification label for the project' + end + + if Gitlab.ee? + params :optional_project_params_ee do + optional :repository_storage, type: String, desc: 'Which storage shard the repository is on. Available only to admins' + optional :approvals_before_merge, type: Integer, desc: 'How many approvers should approve merge request by default' + optional :mirror, type: Boolean, desc: 'Enables pull mirroring in a project' + optional :mirror_trigger_builds, type: Boolean, desc: 'Pull mirroring triggers builds' + end end params :optional_project_params do use :optional_project_params_ce + use :optional_project_params_ee if Gitlab.ee? end end end + + def self.update_params_at_least_one_of + [ + :jobs_enabled, + :resolve_outdated_diff_discussions, + :ci_config_path, + :container_registry_enabled, + :default_branch, + :description, + :issues_enabled, + :lfs_enabled, + :merge_requests_enabled, + :merge_method, + :name, + :only_allow_merge_if_all_discussions_are_resolved, + :only_allow_merge_if_pipeline_succeeds, + :path, + :printing_merge_request_link_enabled, + :public_builds, + :request_access_enabled, + :shared_runners_enabled, + :snippets_enabled, + :tag_list, + :visibility, + :wiki_enabled, + :avatar, + :external_authorization_classification_label + ] + end end end end diff --git a/lib/api/helpers/related_resources_helpers.rb b/lib/api/helpers/related_resources_helpers.rb index 793ae11b41d..9cdde25fe4e 100644 --- a/lib/api/helpers/related_resources_helpers.rb +++ b/lib/api/helpers/related_resources_helpers.rb @@ -13,6 +13,10 @@ module API available?(:merge_requests, project, options[:current_user]) end + def expose_path(path) + Gitlab::Utils.append_path(Gitlab.config.gitlab.relative_url_root, path) + end + def expose_url(path) url_options = Gitlab::Application.routes.default_url_options protocol, host, port, script_name = url_options.values_at(:protocol, :host, :port, :script_name) diff --git a/lib/api/helpers/resource_label_events_helpers.rb b/lib/api/helpers/resource_label_events_helpers.rb new file mode 100644 index 00000000000..23574deb59b --- /dev/null +++ b/lib/api/helpers/resource_label_events_helpers.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module API + module Helpers + module ResourceLabelEventsHelpers + def self.eventable_types + # This is a method instead of a constant, allowing EE to more easily + # extend it. + [Issue, MergeRequest] + end + end + end +end diff --git a/lib/api/helpers/search_helpers.rb b/lib/api/helpers/search_helpers.rb new file mode 100644 index 00000000000..0e052e0e273 --- /dev/null +++ b/lib/api/helpers/search_helpers.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module API + module Helpers + module SearchHelpers + def self.global_search_scopes + # This is a separate method so that EE can redefine it. + %w(projects issues merge_requests milestones snippet_titles snippet_blobs users) + end + + def self.group_search_scopes + # This is a separate method so that EE can redefine it. + %w(projects issues merge_requests milestones users) + end + + def self.project_search_scopes + # This is a separate method so that EE can redefine it. + %w(issues merge_requests milestones notes wiki_blobs commits blobs users) + end + end + end +end diff --git a/lib/api/helpers/services_helpers.rb b/lib/api/helpers/services_helpers.rb new file mode 100644 index 00000000000..953be7f3798 --- /dev/null +++ b/lib/api/helpers/services_helpers.rb @@ -0,0 +1,761 @@ +# coding: utf-8 +# frozen_string_literal: true + +module API + module Helpers + # Helpers module for API::Services + # + # The data structures inside this model are returned using class methods, + # allowing EE to extend them where necessary. + module ServicesHelpers + def self.chat_notification_settings + [ + { + required: true, + name: :webhook, + type: String, + desc: 'The chat webhook' + }, + { + required: false, + name: :username, + type: String, + desc: 'The chat username' + }, + { + required: false, + name: :channel, + type: String, + desc: 'The default chat channel' + } + ].freeze + end + + def self.chat_notification_flags + [ + { + required: false, + name: :notify_only_broken_pipelines, + type: Boolean, + desc: 'Send notifications for broken pipelines' + }, + { + required: false, + name: :notify_only_default_branch, + type: Boolean, + desc: 'Send notifications only for the default branch' + } + ].freeze + end + + def self.chat_notification_channels + [ + { + required: false, + name: :push_channel, + type: String, + desc: 'The name of the channel to receive push_events notifications' + }, + { + required: false, + name: :issue_channel, + type: String, + desc: 'The name of the channel to receive issues_events notifications' + }, + { + required: false, + name: :confidential_issue_channel, + type: String, + desc: 'The name of the channel to receive confidential_issues_events notifications' + }, + { + required: false, + name: :merge_request_channel, + type: String, + desc: 'The name of the channel to receive merge_requests_events notifications' + }, + { + required: false, + name: :note_channel, + type: String, + desc: 'The name of the channel to receive note_events notifications' + }, + { + required: false, + name: :tag_push_channel, + type: String, + desc: 'The name of the channel to receive tag_push_events notifications' + }, + { + required: false, + name: :pipeline_channel, + type: String, + desc: 'The name of the channel to receive pipeline_events notifications' + }, + { + required: false, + name: :wiki_page_channel, + type: String, + desc: 'The name of the channel to receive wiki_page_events notifications' + } + ].freeze + end + + def self.chat_notification_events + [ + { + required: false, + name: :push_events, + type: Boolean, + desc: 'Enable notifications for push_events' + }, + { + required: false, + name: :issues_events, + type: Boolean, + desc: 'Enable notifications for issues_events' + }, + { + required: false, + name: :confidential_issues_events, + type: Boolean, + desc: 'Enable notifications for confidential_issues_events' + }, + { + required: false, + name: :merge_requests_events, + type: Boolean, + desc: 'Enable notifications for merge_requests_events' + }, + { + required: false, + name: :note_events, + type: Boolean, + desc: 'Enable notifications for note_events' + }, + { + required: false, + name: :tag_push_events, + type: Boolean, + desc: 'Enable notifications for tag_push_events' + }, + { + required: false, + name: :pipeline_events, + type: Boolean, + desc: 'Enable notifications for pipeline_events' + }, + { + required: false, + name: :wiki_page_events, + type: Boolean, + desc: 'Enable notifications for wiki_page_events' + } + ].freeze + end + + def self.services + { + 'asana' => [ + { + required: true, + name: :api_key, + type: String, + desc: 'User API token' + }, + { + required: false, + name: :restrict_to_branch, + type: String, + desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches' + } + ], + 'assembla' => [ + { + required: true, + name: :token, + type: String, + desc: 'The authentication token' + }, + { + required: false, + name: :subdomain, + type: String, + desc: 'Subdomain setting' + } + ], + 'bamboo' => [ + { + required: true, + name: :bamboo_url, + type: String, + desc: 'Bamboo root URL like https://bamboo.example.com' + }, + { + required: true, + name: :build_key, + type: String, + desc: 'Bamboo build plan key like' + }, + { + required: true, + name: :username, + type: String, + desc: 'A user with API access, if applicable' + }, + { + required: true, + name: :password, + type: String, + desc: 'Password of the user' + } + ], + 'bugzilla' => [ + { + required: true, + name: :new_issue_url, + type: String, + desc: 'New issue URL' + }, + { + required: true, + name: :issues_url, + type: String, + desc: 'Issues URL' + }, + { + required: true, + name: :project_url, + type: String, + desc: 'Project URL' + }, + { + required: false, + name: :description, + type: String, + desc: 'Description' + }, + { + required: false, + name: :title, + type: String, + desc: 'Title' + } + ], + 'buildkite' => [ + { + required: true, + name: :token, + type: String, + desc: 'Buildkite project GitLab token' + }, + { + required: true, + name: :project_url, + type: String, + desc: 'The buildkite project URL' + }, + { + required: false, + name: :enable_ssl_verification, + type: Boolean, + desc: 'Enable SSL verification for communication' + } + ], + 'campfire' => [ + { + required: true, + name: :token, + type: String, + desc: 'Campfire token' + }, + { + required: false, + name: :subdomain, + type: String, + desc: 'Campfire subdomain' + }, + { + required: false, + name: :room, + type: String, + desc: 'Campfire room' + } + ], + 'custom-issue-tracker' => [ + { + required: true, + name: :new_issue_url, + type: String, + desc: 'New issue URL' + }, + { + required: true, + name: :issues_url, + type: String, + desc: 'Issues URL' + }, + { + required: true, + name: :project_url, + type: String, + desc: 'Project URL' + }, + { + required: false, + name: :description, + type: String, + desc: 'Description' + }, + { + required: false, + name: :title, + type: String, + desc: 'Title' + } + ], + 'discord' => [ + { + required: true, + name: :webhook, + type: String, + desc: 'Discord webhook. e.g. https://discordapp.com/api/webhooks/…' + } + ], + 'drone-ci' => [ + { + required: true, + name: :token, + type: String, + desc: 'Drone CI token' + }, + { + required: true, + name: :drone_url, + type: String, + desc: 'Drone CI URL' + }, + { + required: false, + name: :enable_ssl_verification, + type: Boolean, + desc: 'Enable SSL verification for communication' + } + ], + 'emails-on-push' => [ + { + required: true, + name: :recipients, + type: String, + desc: 'Comma-separated list of recipient email addresses' + }, + { + required: false, + name: :disable_diffs, + type: Boolean, + desc: 'Disable code diffs' + }, + { + required: false, + name: :send_from_committer_email, + type: Boolean, + desc: 'Send from committer' + } + ], + 'external-wiki' => [ + { + required: true, + name: :external_wiki_url, + type: String, + desc: 'The URL of the external Wiki' + } + ], + 'flowdock' => [ + { + required: true, + name: :token, + type: String, + desc: 'Flowdock token' + } + ], + 'hangouts-chat' => [ + { + required: true, + name: :webhook, + type: String, + desc: 'The Hangouts Chat webhook. e.g. https://chat.googleapis.com/v1/spaces…' + }, + chat_notification_events + ].flatten, + 'hipchat' => [ + { + required: true, + name: :token, + type: String, + desc: 'The room token' + }, + { + required: false, + name: :room, + type: String, + desc: 'The room name or ID' + }, + { + required: false, + name: :color, + type: String, + desc: 'The room color' + }, + { + required: false, + name: :notify, + type: Boolean, + desc: 'Enable notifications' + }, + { + required: false, + name: :api_version, + type: String, + desc: 'Leave blank for default (v2)' + }, + { + required: false, + name: :server, + type: String, + desc: 'Leave blank for default. https://hipchat.example.com' + } + ], + 'irker' => [ + { + required: true, + name: :recipients, + type: String, + desc: 'Recipients/channels separated by whitespaces' + }, + { + required: false, + name: :default_irc_uri, + type: String, + desc: 'Default: irc://irc.network.net:6697' + }, + { + required: false, + name: :server_host, + type: String, + desc: 'Server host. Default localhost' + }, + { + required: false, + name: :server_port, + type: Integer, + desc: 'Server port. Default 6659' + }, + { + required: false, + name: :colorize_messages, + type: Boolean, + desc: 'Colorize messages' + } + ], + 'jira' => [ + { + required: true, + name: :url, + type: String, + desc: 'The base URL to the JIRA instance web interface which is being linked to this GitLab project. E.g., https://jira.example.com' + }, + { + required: false, + name: :api_url, + type: String, + desc: 'The base URL to the JIRA instance API. Web URL value will be used if not set. E.g., https://jira-api.example.com' + }, + { + required: true, + name: :username, + type: String, + desc: 'The username of the user created to be used with GitLab/JIRA' + }, + { + required: true, + name: :password, + type: String, + desc: 'The password of the user created to be used with GitLab/JIRA' + }, + { + required: false, + name: :jira_issue_transition_id, + type: String, + desc: 'The ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`' + } + ], + 'kubernetes' => [ + { + required: true, + name: :namespace, + type: String, + desc: 'The Kubernetes namespace to use' + }, + { + required: true, + name: :api_url, + type: String, + desc: 'The URL to the Kubernetes cluster API, e.g., https://kubernetes.example.com' + }, + { + required: true, + name: :token, + type: String, + desc: 'The service token to authenticate against the Kubernetes cluster with' + }, + { + required: false, + name: :ca_pem, + type: String, + desc: 'A custom certificate authority bundle to verify the Kubernetes cluster with (PEM format)' + } + ], + 'mattermost-slash-commands' => [ + { + required: true, + name: :token, + type: String, + desc: 'The Mattermost token' + } + ], + 'slack-slash-commands' => [ + { + required: true, + name: :token, + type: String, + desc: 'The Slack token' + } + ], + 'packagist' => [ + { + required: true, + name: :username, + type: String, + desc: 'The username' + }, + { + required: true, + name: :token, + type: String, + desc: 'The Packagist API token' + }, + { + required: false, + name: :server, + type: String, + desc: 'The server' + } + ], + 'pipelines-email' => [ + { + required: true, + name: :recipients, + type: String, + desc: 'Comma-separated list of recipient email addresses' + }, + { + required: false, + name: :notify_only_broken_pipelines, + type: Boolean, + desc: 'Notify only broken pipelines' + } + ], + 'pivotaltracker' => [ + { + required: true, + name: :token, + type: String, + desc: 'The Pivotaltracker token' + }, + { + required: false, + name: :restrict_to_branch, + type: String, + desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches.' + } + ], + 'prometheus' => [ + { + required: true, + name: :api_url, + type: String, + desc: 'Prometheus API Base URL, like http://prometheus.example.com/' + } + ], + 'pushover' => [ + { + required: true, + name: :api_key, + type: String, + desc: 'The application key' + }, + { + required: true, + name: :user_key, + type: String, + desc: 'The user key' + }, + { + required: true, + name: :priority, + type: String, + desc: 'The priority' + }, + { + required: true, + name: :device, + type: String, + desc: 'Leave blank for all active devices' + }, + { + required: true, + name: :sound, + type: String, + desc: 'The sound of the notification' + } + ], + 'redmine' => [ + { + required: true, + name: :new_issue_url, + type: String, + desc: 'The new issue URL' + }, + { + required: true, + name: :project_url, + type: String, + desc: 'The project URL' + }, + { + required: true, + name: :issues_url, + type: String, + desc: 'The issues URL' + }, + { + required: false, + name: :description, + type: String, + desc: 'The description of the tracker' + } + ], + 'youtrack' => [ + { + required: true, + name: :project_url, + type: String, + desc: 'The project URL' + }, + { + required: true, + name: :issues_url, + type: String, + desc: 'The issues URL' + }, + { + required: false, + name: :description, + type: String, + desc: 'The description of the tracker' + } + ], + 'slack' => [ + chat_notification_settings, + chat_notification_flags, + chat_notification_channels, + chat_notification_events + ].flatten, + 'microsoft-teams' => [ + { + required: true, + name: :webhook, + type: String, + desc: 'The Microsoft Teams webhook. e.g. https://outlook.office.com/webhook/…' + } + ], + 'mattermost' => [ + chat_notification_settings, + chat_notification_flags, + chat_notification_channels, + chat_notification_events + ].flatten, + 'teamcity' => [ + { + required: true, + name: :teamcity_url, + type: String, + desc: 'TeamCity root URL like https://teamcity.example.com' + }, + { + required: true, + name: :build_type, + type: String, + desc: 'Build configuration ID' + }, + { + required: true, + name: :username, + type: String, + desc: 'A user with permissions to trigger a manual build' + }, + { + required: true, + name: :password, + type: String, + desc: 'The password of the user' + } + ] + } + end + + def self.service_classes + [ + ::AsanaService, + ::AssemblaService, + ::BambooService, + ::BugzillaService, + ::BuildkiteService, + ::CampfireService, + ::CustomIssueTrackerService, + ::DiscordService, + ::DroneCiService, + ::EmailsOnPushService, + ::ExternalWikiService, + ::FlowdockService, + ::HangoutsChatService, + ::HipchatService, + ::IrkerService, + ::JiraService, + ::KubernetesService, + ::MattermostSlashCommandsService, + ::SlackSlashCommandsService, + ::PackagistService, + ::PipelinesEmailService, + ::PivotaltrackerService, + ::PrometheusService, + ::PushoverService, + ::RedmineService, + ::YoutrackService, + ::SlackService, + ::MattermostService, + ::MicrosoftTeamsService, + ::TeamcityService + ] + end + + def self.development_service_classes + [ + ::MockCiService, + ::MockDeploymentService, + ::MockMonitoringService + ] + end + end + end +end diff --git a/lib/api/import_github.rb b/lib/api/import_github.rb index bb4e536cf57..e7504051808 100644 --- a/lib/api/import_github.rb +++ b/lib/api/import_github.rb @@ -20,6 +20,10 @@ module API def provider :github end + + def provider_unauthorized + error!("Access denied to your #{Gitlab::ImportSources.title(provider.to_s)} account.", 401) + end end desc 'Import a GitHub project' do diff --git a/lib/api/internal.rb b/lib/api/internal.rb index 70b32f7d758..c82fd230d7a 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -15,6 +15,12 @@ module API status code { status: success, message: message }.merge(extra_options).compact end + + def lfs_authentication_url(project) + # This is a separate method so that EE can alter its behaviour more + # easily. + project.http_url_to_repo + end end namespace 'internal' do @@ -53,7 +59,7 @@ module API actor end - access_checker_klass = wiki? ? Gitlab::GitAccessWiki : Gitlab::GitAccess + access_checker_klass = repo_type.access_checker_class access_checker = access_checker_klass.new(actor, project, protocol, authentication_abilities: ssh_authentication_abilities, namespace_path: namespace_path, project_path: project_path, @@ -81,12 +87,8 @@ module API gl_id: Gitlab::GlId.gl_id(user), gl_username: user&.username, git_config_options: [], - - # This repository_path is a bogus value but gitlab-shell still requires - # its presence. https://gitlab.com/gitlab-org/gitlab-shell/issues/135 - repository_path: '/', - - gitaly: gitaly_payload(params[:action]) + gitaly: gitaly_payload(params[:action]), + gl_console_messages: check_result.console_messages } # Custom option for git-receive-pack command @@ -118,7 +120,9 @@ module API raise ActiveRecord::RecordNotFound.new("No key_id or user_id passed!") end - Gitlab::LfsToken.new(actor).authentication_payload(project.http_url_to_repo) + Gitlab::LfsToken + .new(actor) + .authentication_payload(lfs_authentication_url(project)) end # rubocop: enable CodeReuse/ActiveRecord @@ -252,19 +256,29 @@ module API post '/post_receive' do status 200 + output = {} # Messages to gitlab-shell + user = identify(params[:identifier]) + project = Gitlab::GlRepository.parse(params[:gl_repository]).first + push_options = Gitlab::PushOptions.new(params[:push_options]) + PostReceive.perform_async(params[:gl_repository], params[:identifier], - params[:changes], params[:push_options].to_a) + params[:changes], push_options.as_json) + + if Feature.enabled?(:mr_push_options, default_enabled: true) + Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/61359') + + mr_options = push_options.get(:merge_request) + output.merge!(process_mr_push_options(mr_options, project, user, params[:changes])) if mr_options.present? + end + broadcast_message = BroadcastMessage.current&.last&.message reference_counter_decreased = Gitlab::ReferenceCounter.new(params[:gl_repository]).decrease - output = { - merge_request_urls: merge_request_urls, + output.merge!( broadcast_message: broadcast_message, - reference_counter_decreased: reference_counter_decreased - } - - project = Gitlab::GlRepository.parse(params[:gl_repository]).first - user = identify(params[:identifier]) + reference_counter_decreased: reference_counter_decreased, + merge_request_urls: merge_request_urls + ) # A user is not guaranteed to be returned; an orphaned write deploy # key could be used diff --git a/lib/api/issues.rb b/lib/api/issues.rb index f43f4d961d6..d0a93b77951 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -8,15 +8,6 @@ module API helpers ::Gitlab::IssuableMetadata - # EE::API::Issues would override the following helpers - helpers do - params :issues_params_ee do - end - - params :issue_params_ee do - end - end - helpers do # rubocop: disable CodeReuse/ActiveRecord def find_issues(args = {}) @@ -28,13 +19,23 @@ module API args[:scope] = args[:scope].underscore if args[:scope] issues = IssuesFinder.new(current_user, args).execute - .preload(:assignees, :labels, :notes, :timelogs, :project, :author, :closed_by) + .with_api_entity_associations issues.reorder(order_options_with_tie_breaker) end # rubocop: enable CodeReuse/ActiveRecord + if Gitlab.ee? + params :issues_params_ee do + optional :weight, types: [Integer, String], integer_none_any: true, desc: 'The weight of the issue' + end + + params :issue_params_ee do + optional :weight, type: Integer, desc: 'The weight of the issue' + end + end + params :issues_params do - optional :labels, type: String, desc: 'Comma-separated list of label names' + optional :labels, type: Array[String], coerce_with: Validations::Types::LabelsList.coerce, desc: 'Comma-separated list of label names' optional :milestone, type: String, desc: 'Milestone title' optional :order_by, type: String, values: %w[created_at updated_at], default: 'created_at', desc: 'Return issues ordered by `created_at` or `updated_at` fields.' @@ -57,7 +58,7 @@ module API optional :confidential, type: Boolean, desc: 'Filter confidential or public issues' use :pagination - use :issues_params_ee + use :issues_params_ee if Gitlab.ee? end params :issue_params do @@ -65,12 +66,12 @@ module API optional :assignee_ids, type: Array[Integer], desc: 'The array of user IDs to assign issue' optional :assignee_id, type: Integer, desc: '[Deprecated] The ID of a user to assign issue' optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign issue' - optional :labels, type: String, desc: 'Comma-separated list of label names' + optional :labels, type: Array[String], coerce_with: Validations::Types::LabelsList.coerce, desc: 'Comma-separated list of label names' optional :due_date, type: String, desc: 'Date string in the format YEAR-MONTH-DAY' optional :confidential, type: Boolean, desc: 'Boolean parameter if the issue should be confidential' optional :discussion_locked, type: Boolean, desc: " Boolean parameter indicating if the issue's discussion is locked" - use :issue_params_ee + use :issue_params_ee if Gitlab.ee? end end @@ -191,6 +192,7 @@ module API params.delete(:iid) unless current_user.can?(:set_issue_iid, user_project) issue_params = declared_params(include_missing: false) + issue_params[:system_note_timestamp] = params[:created_at] issue_params = convert_parameters_from_legacy_format(issue_params) @@ -219,8 +221,8 @@ module API desc: 'Date time when the issue was updated. Available only for admins and project owners.' optional :state_event, type: String, values: %w[reopen close], desc: 'State of the issue' use :issue_params - at_least_one_of :title, :description, :assignee_ids, :assignee_id, :milestone_id, :discussion_locked, - :labels, :created_at, :due_date, :confidential, :state_event + + at_least_one_of(*Helpers::IssuesHelpers.update_params_at_least_one_of) end # rubocop: disable CodeReuse/ActiveRecord put ':id/issues/:issue_iid' do @@ -229,9 +231,13 @@ module API issue = user_project.issues.find_by!(iid: params.delete(:issue_iid)) authorize! :update_issue, issue - # Setting created_at time only allowed for admins and project/group owners - unless current_user.admin? || user_project.owner == current_user || current_user.owned_groups.include?(user_project.owner) - params.delete(:updated_at) + # Setting updated_at only allowed for admins and owners as well + if params[:updated_at].present? + if current_user.admin? || user_project.owner == current_user || current_user.owned_groups.include?(user_project.owner) + issue.system_note_timestamp = params[:updated_at] + else + params.delete(:updated_at) + end end update_params = declared_params(include_missing: false).merge(request: request, api: true) @@ -306,10 +312,10 @@ module API merge_requests = ::Issues::ReferencedMergeRequestsService.new(user_project, current_user) .execute(issue) - .flatten + .first present paginate(::Kaminari.paginate_array(merge_requests)), - with: Entities::MergeRequestBasic, + with: Entities::MergeRequest, current_user: current_user, project: user_project end diff --git a/lib/api/job_artifacts.rb b/lib/api/job_artifacts.rb index 933bd067e26..e7fed55170e 100644 --- a/lib/api/job_artifacts.rb +++ b/lib/api/job_artifacts.rb @@ -109,6 +109,22 @@ module API status 200 present build, with: Entities::Job end + + desc 'Delete the artifacts files from a job' do + detail 'This feature was introduced in GitLab 11.9' + end + params do + requires :job_id, type: Integer, desc: 'The ID of a job' + end + delete ':id/jobs/:job_id/artifacts' do + authorize_destroy_artifacts! + build = find_build!(params[:job_id]) + authorize!(:destroy_artifacts, build) + + build.erase_erasable_artifacts! + + status :no_content + end end end end diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 44f1e81caf2..daa98c22e5e 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -12,11 +12,15 @@ module API helpers do params :optional_params_ee do end + + params :optional_merge_requests_search_params do + end end def self.update_params_at_least_one_of %i[ assignee_id + assignee_ids description labels milestone_id @@ -45,7 +49,7 @@ module API return merge_requests if args[:view] == 'simple' merge_requests - .preload(:notes, :author, :assignee, :milestone, :latest_merge_request_diff, :labels, :timelogs, metrics: [:latest_closed_by, :merged_by]) + .with_api_entity_associations end # rubocop: enable CodeReuse/ActiveRecord @@ -95,7 +99,7 @@ module API optional :sort, type: String, values: %w[asc desc], default: 'desc', desc: 'Return merge requests sorted in `asc` or `desc` order.' optional :milestone, type: String, desc: 'Return merge requests for a specific milestone' - optional :labels, type: String, desc: 'Comma-separated list of label names' + optional :labels, type: Array[String], coerce_with: Validations::Types::LabelsList.coerce, desc: 'Comma-separated list of label names' optional :created_after, type: DateTime, desc: 'Return merge requests created after the specified time' optional :created_before, type: DateTime, desc: 'Return merge requests created before the specified time' optional :updated_after, type: DateTime, desc: 'Return merge requests updated after the specified time' @@ -108,10 +112,13 @@ module API desc: 'Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`' optional :my_reaction_emoji, type: String, desc: 'Return issues reacted by the authenticated user by the given emoji' optional :source_branch, type: String, desc: 'Return merge requests with the given source branch' + optional :source_project_id, type: Integer, desc: 'Return merge requests with the given source project id' optional :target_branch, type: String, desc: 'Return merge requests with the given target branch' optional :search, type: String, desc: 'Search merge requests for text present in the title, description, or any combination of these' optional :in, type: String, desc: '`title`, `description`, or a string joining them with comma' optional :wip, type: String, values: %w[yes no], desc: 'Search merge requests for WIP in the title' + + use :optional_merge_requests_search_params use :pagination end end @@ -178,9 +185,10 @@ module API 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], 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 :labels, type: String, desc: 'Comma-separated list of label names' - optional :remove_source_branch, type: Boolean, desc: 'Delete source branch when merging' + optional :labels, type: Array[String], coerce_with: Validations::Types::LabelsList.coerce, desc: 'Comma-separated list of label names' + 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' optional :squash, type: Grape::API::Boolean, desc: 'When true, the commits will be squashed into a single commit on merge' @@ -225,6 +233,7 @@ module API mr_params = declared_params(include_missing: false) mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) + mr_params = convert_parameters_from_legacy_format(mr_params) merge_request = ::MergeRequests::CreateService.new(user_project, current_user, mr_params).execute @@ -327,7 +336,8 @@ module API merge_request = find_merge_request_with_access(params.delete(:merge_request_iid), :update_merge_request) mr_params = declared_params(include_missing: false) - mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params[:remove_source_branch].present? + mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params.has_key?(:remove_source_branch) + mr_params = convert_parameters_from_legacy_format(mr_params) merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, mr_params).execute(merge_request) @@ -357,6 +367,10 @@ module API merge_request = find_project_merge_request(params[:merge_request_iid]) merge_when_pipeline_succeeds = to_boolean(params[:merge_when_pipeline_succeeds]) + if merge_when_pipeline_succeeds || merge_request.merge_when_pipeline_succeeds + render_api_error!('Not allowed: pipeline does not exist', 405) unless merge_request.head_pipeline + end + # Merge request can not be merged # because user dont have permissions to push into target branch unauthorized! unless merge_request.can_be_merged_by?(current_user) diff --git a/lib/api/milestone_responses.rb b/lib/api/milestone_responses.rb index a0ca39b69d4..62e159ab003 100644 --- a/lib/api/milestone_responses.rb +++ b/lib/api/milestone_responses.rb @@ -16,6 +16,7 @@ module API optional :state, type: String, values: %w[active closed all], default: 'all', desc: 'Return "active", "closed", or "all" milestones' optional :iids, type: Array[Integer], desc: 'The IIDs of the milestones' + optional :title, type: String, desc: 'The title of the milestones' optional :search, type: String, desc: 'The search criteria for the title or description of the milestone' use :pagination end @@ -33,6 +34,7 @@ module API milestones = parent.milestones.order_id_desc milestones = Milestone.filter_by_state(milestones, params[:state]) milestones = filter_by_iid(milestones, params[:iids]) if params[:iids].present? + milestones = filter_by_title(milestones, params[:title]) if params[:title] milestones = filter_by_search(milestones, params[:search]) if params[:search] present paginate(milestones), with: Entities::Milestone diff --git a/lib/api/namespaces.rb b/lib/api/namespaces.rb index 3cc09f6ac3f..77ecb3e7cde 100644 --- a/lib/api/namespaces.rb +++ b/lib/api/namespaces.rb @@ -44,6 +44,8 @@ module API requires :id, type: String, desc: "Namespace's ID or path" end get ':id', requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + user_namespace = find_namespace!(params[:id]) + present user_namespace, with: Entities::Namespace, current_user: current_user end end diff --git a/lib/api/notes.rb b/lib/api/notes.rb index f7bd092ce50..416cf39d3ec 100644 --- a/lib/api/notes.rb +++ b/lib/api/notes.rb @@ -7,9 +7,7 @@ module API before { authenticate! } - NOTEABLE_TYPES = [Issue, MergeRequest, Snippet].freeze - - NOTEABLE_TYPES.each do |noteable_type| + Helpers::NotesHelpers.noteable_types.each do |noteable_type| parent_type = noteable_type.parent_class.to_s.underscore noteables_str = noteable_type.to_s.underscore.pluralize diff --git a/lib/api/pipeline_schedules.rb b/lib/api/pipeline_schedules.rb index c86b50d3736..1d1ef1afc6b 100644 --- a/lib/api/pipeline_schedules.rb +++ b/lib/api/pipeline_schedules.rb @@ -118,6 +118,7 @@ module API requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id' requires :key, type: String, desc: 'The key of the variable' requires :value, type: String, desc: 'The value of the variable' + optional :variable_type, type: String, values: Ci::PipelineScheduleVariable.variable_types.keys, desc: 'The type of variable, must be one of env_var or file. Defaults to env_var' end post ':id/pipeline_schedules/:pipeline_schedule_id/variables' do authorize! :update_pipeline_schedule, pipeline_schedule @@ -138,6 +139,7 @@ module API requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id' requires :key, type: String, desc: 'The key of the variable' optional :value, type: String, desc: 'The value of the variable' + optional :variable_type, type: String, values: Ci::PipelineScheduleVariable.variable_types.keys, desc: 'The type of variable, must be one of env_var or file' end put ':id/pipeline_schedules/:pipeline_schedule_id/variables/:key' do authorize! :update_pipeline_schedule, pipeline_schedule diff --git a/lib/api/pipelines.rb b/lib/api/pipelines.rb index ac8fe98e55e..667bf1ec801 100644 --- a/lib/api/pipelines.rb +++ b/lib/api/pipelines.rb @@ -81,6 +81,19 @@ module API present pipeline, with: Entities::Pipeline end + desc 'Gets the variables for a given pipeline' do + detail 'This feature was introduced in GitLab 11.11' + success Entities::Variable + end + params do + requires :pipeline_id, type: Integer, desc: 'The pipeline ID' + end + get ':id/pipelines/:pipeline_id/variables' do + authorize! :read_pipeline_variable, pipeline + + present pipeline.variables, with: Entities::Variable + end + desc 'Deletes a pipeline' do detail 'This feature was introduced in GitLab 11.6' http_codes [[204, 'Pipeline was deleted'], [403, 'Forbidden']] @@ -124,7 +137,7 @@ module API pipeline.cancel_running status 200 - present pipeline.reload, with: Entities::Pipeline + present pipeline.reset, with: Entities::Pipeline end end diff --git a/lib/api/project_clusters.rb b/lib/api/project_clusters.rb index c96261a7b57..dcc8d94fb79 100644 --- a/lib/api/project_clusters.rb +++ b/lib/api/project_clusters.rb @@ -53,6 +53,8 @@ module API params do requires :name, type: String, desc: 'Cluster name' optional :enabled, type: Boolean, default: true, desc: 'Determines if cluster is active or not, defaults to true' + optional :domain, type: String, desc: 'Cluster base domain' + optional :managed, type: Boolean, default: true, desc: 'Determines if GitLab will manage namespaces and service accounts for this cluster, defaults to true' requires :platform_kubernetes_attributes, type: Hash, desc: %q(Platform Kubernetes data) do requires :api_url, type: String, allow_blank: false, desc: 'URL to access the Kubernetes API' requires :token, type: String, desc: 'Token to authenticate against Kubernetes' @@ -83,6 +85,7 @@ module API params do requires :cluster_id, type: Integer, desc: 'The cluster ID' optional :name, type: String, desc: 'Cluster name' + optional :domain, type: String, desc: 'Cluster base domain' optional :platform_kubernetes_attributes, type: Hash, desc: %q(Platform Kubernetes data) do optional :api_url, type: String, desc: 'URL to access the Kubernetes API' optional :token, type: String, desc: 'Token to authenticate against Kubernetes' diff --git a/lib/api/project_events.rb b/lib/api/project_events.rb new file mode 100644 index 00000000000..734311e1142 --- /dev/null +++ b/lib/api/project_events.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module API + class ProjectEvents < Grape::API + include PaginationParams + include APIGuard + helpers ::API::Helpers::EventsHelpers + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + desc "List a Project's visible events" do + success Entities::Event + end + params do + use :pagination + use :event_filter_params + use :sort_params + end + + get ":id/events" do + events = find_events(user_project) + + present_events(events) + end + end + end +end diff --git a/lib/api/project_milestones.rb b/lib/api/project_milestones.rb index ca24742b7a3..9ecbf37b49a 100644 --- a/lib/api/project_milestones.rb +++ b/lib/api/project_milestones.rb @@ -103,17 +103,15 @@ module API detail 'This feature was introduced in GitLab 11.9' end post ':id/milestones/:milestone_id/promote' do - begin - authorize! :admin_milestone, user_project - authorize! :admin_milestone, user_project.group + authorize! :admin_milestone, user_project + authorize! :admin_milestone, user_project.group - milestone = user_project.milestones.find(params[:milestone_id]) - Milestones::PromoteService.new(user_project, current_user).execute(milestone) + milestone = user_project.milestones.find(params[:milestone_id]) + Milestones::PromoteService.new(user_project, current_user).execute(milestone) - status(200) - rescue Milestones::PromoteService::PromoteMilestoneError => error - render_api_error!(error.message, 400) - end + status(200) + rescue Milestones::PromoteService::PromoteMilestoneError => error + render_api_error!(error.message, 400) end end end diff --git a/lib/api/projects.rb b/lib/api/projects.rb index b23fe6cd4e7..cb0106592f5 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -11,12 +11,20 @@ module API before { authenticate_non_get! } helpers do - params :optional_filter_params_ee do - # EE::API::Projects would override this helper - end + if Gitlab.ee? + params :optional_filter_params_ee do + optional :wiki_checksum_failed, type: Grape::API::Boolean, default: false, desc: 'Limit by projects where wiki checksum is failed' + optional :repository_checksum_failed, type: Grape::API::Boolean, default: false, desc: 'Limit by projects where repository checksum is failed' + end - params :optional_update_params_ee do - # EE::API::Projects would override this helper + params :optional_update_params_ee do + optional :mirror_user_id, type: Integer, desc: 'User responsible for all the activity surrounding a pull mirror event' + optional :only_mirror_protected_branches, type: Grape::API::Boolean, desc: 'Only mirror protected branches' + optional :mirror_overwrites_diverged_branches, type: Grape::API::Boolean, desc: 'Pull mirror overwrites diverged branches' + optional :import_url, type: String, desc: 'URL from which the project is imported' + optional :packages_enabled, type: Grape::API::Boolean, desc: 'Enable project packages feature' + optional :fallback_approvals_required, type: Integer, desc: 'Overall approvals required when no rule is present' + end end # EE::API::Projects would override this method @@ -35,34 +43,6 @@ module API end end - def self.update_params_at_least_one_of - [ - :jobs_enabled, - :resolve_outdated_diff_discussions, - :ci_config_path, - :container_registry_enabled, - :default_branch, - :description, - :issues_enabled, - :lfs_enabled, - :merge_requests_enabled, - :merge_method, - :name, - :only_allow_merge_if_all_discussions_are_resolved, - :only_allow_merge_if_pipeline_succeeds, - :path, - :printing_merge_request_link_enabled, - :public_builds, - :request_access_enabled, - :shared_runners_enabled, - :snippets_enabled, - :tag_list, - :visibility, - :wiki_enabled, - :avatar - ] - end - helpers do params :statistics_params do optional :statistics, type: Boolean, default: false, desc: 'Include project statistics' @@ -97,7 +77,7 @@ module API optional :with_programming_language, type: String, desc: 'Limit to repositories which use the given programming language' optional :min_access_level, type: Integer, values: Gitlab::Access.all_values, desc: 'Limit by minimum access level of authenticated user' - use :optional_filter_params_ee + use :optional_filter_params_ee if Gitlab.ee? end params :create_params do @@ -184,7 +164,8 @@ module API if project.saved? present project, with: Entities::Project, - user_can_admin_project: can?(current_user, :admin_project, project) + user_can_admin_project: can?(current_user, :admin_project, project), + current_user: current_user else if project.errors[:limit_reached].present? error!(project.errors[:limit_reached], 403) @@ -217,7 +198,8 @@ module API if project.saved? present project, with: Entities::Project, - user_can_admin_project: can?(current_user, :admin_project, project) + user_can_admin_project: can?(current_user, :admin_project, project), + current_user: current_user else render_validation_error!(project) end @@ -281,7 +263,8 @@ module API conflict!(forked_project.errors.messages) else present forked_project, with: Entities::Project, - user_can_admin_project: can?(current_user, :admin_project, forked_project) + user_can_admin_project: can?(current_user, :admin_project, forked_project), + current_user: current_user end end @@ -313,8 +296,9 @@ module API optional :path, type: String, desc: 'The path of the repository' use :optional_project_params + use :optional_update_params_ee if Gitlab.ee? - at_least_one_of(*::API::Projects.update_params_at_least_one_of) + at_least_one_of(*Helpers::ProjectsHelpers.update_params_at_least_one_of) end put ':id' do authorize_admin_project @@ -330,7 +314,8 @@ module API if result[:status] == :success present user_project, with: Entities::Project, - user_can_admin_project: can?(current_user, :admin_project, user_project) + user_can_admin_project: can?(current_user, :admin_project, user_project), + current_user: current_user else render_validation_error!(user_project) end @@ -344,7 +329,7 @@ module API ::Projects::UpdateService.new(user_project, current_user, archived: true).execute - present user_project, with: Entities::Project + present user_project, with: Entities::Project, current_user: current_user end desc 'Unarchive a project' do @@ -355,7 +340,7 @@ module API ::Projects::UpdateService.new(@project, current_user, archived: false).execute - present user_project, with: Entities::Project + present user_project, with: Entities::Project, current_user: current_user end desc 'Star a project' do @@ -366,9 +351,9 @@ module API not_modified! else current_user.toggle_star(user_project) - user_project.reload + user_project.reset - present user_project, with: Entities::Project + present user_project, with: Entities::Project, current_user: current_user end end @@ -378,9 +363,9 @@ module API post ':id/unstar' do if current_user.starred?(user_project) current_user.toggle_star(user_project) - user_project.reload + user_project.reset - present user_project, with: Entities::Project + present user_project, with: Entities::Project, current_user: current_user else not_modified! end @@ -388,11 +373,9 @@ module API desc 'Get languages in project repository' get ':id/languages' do - if user_project.repository_languages.present? - user_project.repository_languages.map { |l| [l.name, l.share] }.to_h - else - user_project.repository.languages.map { |language| language.values_at(:label, :value) }.to_h - end + ::Projects::RepositoryLanguagesService + .new(user_project, current_user) + .execute.map { |lang| [lang.name, lang.share] }.to_h end desc 'Remove a project' @@ -420,7 +403,7 @@ module API result = ::Projects::ForkService.new(fork_from_project, current_user).execute(user_project) if result - present user_project.reload, with: Entities::Project + present user_project.reset, with: Entities::Project, current_user: current_user else render_api_error!("Project already forked", 409) if user_project.forked? end @@ -442,27 +425,24 @@ module API end params do requires :group_id, type: Integer, desc: 'The ID of a group' - requires :group_access, type: Integer, values: Gitlab::Access.values, desc: 'The group access level' + requires :group_access, type: Integer, values: Gitlab::Access.values, as: :link_group_access, desc: 'The group access level' optional :expires_at, type: Date, desc: 'Share expiration date' end post ":id/share" do authorize! :admin_project, user_project group = Group.find_by_id(params[:group_id]) - unless group && can?(current_user, :read_group, group) - not_found!('Group') - end - unless user_project.allowed_to_share_with_group? break render_api_error!("The project sharing with group is disabled", 400) end - link = user_project.project_group_links.new(declared_params(include_missing: false)) + result = ::Projects::GroupLinks::CreateService.new(user_project, current_user, declared_params(include_missing: false)) + .execute(group) - if link.save - present link, with: Entities::ProjectGroupLink + if result[:status] == :success + present result[:link], with: Entities::ProjectGroupLink else - render_api_error!(link.errors.full_messages.first, 409) + render_api_error!(result[:message], result[:http_status]) end end @@ -526,7 +506,7 @@ module API result = ::Projects::TransferService.new(user_project, current_user).execute(namespace) if result - present user_project, with: Entities::Project + present user_project, with: Entities::Project, current_user: current_user else render_api_error!("Failed to transfer project #{user_project.errors.messages}", 400) end diff --git a/lib/api/protected_branches.rb b/lib/api/protected_branches.rb index 5af43448727..f8cce1ed784 100644 --- a/lib/api/protected_branches.rb +++ b/lib/api/protected_branches.rb @@ -51,6 +51,30 @@ module API optional :merge_access_level, type: Integer, values: ProtectedBranch::MergeAccessLevel.allowed_access_levels, desc: 'Access levels allowed to merge (defaults: `40`, maintainer access level)' + + if Gitlab.ee? + optional :unprotect_access_level, type: Integer, + values: ProtectedBranch::UnprotectAccessLevel.allowed_access_levels, + desc: 'Access levels allowed to unprotect (defaults: `40`, maintainer access level)' + + optional :allowed_to_push, type: Array, desc: 'An array of users/groups allowed to push' do + optional :access_level, type: Integer, values: ProtectedBranch::PushAccessLevel.allowed_access_levels + optional :user_id, type: Integer + optional :group_id, type: Integer + end + + optional :allowed_to_merge, type: Array, desc: 'An array of users/groups allowed to merge' do + optional :access_level, type: Integer, values: ProtectedBranch::MergeAccessLevel.allowed_access_levels + optional :user_id, type: Integer + optional :group_id, type: Integer + end + + optional :allowed_to_unprotect, type: Array, desc: 'An array of users/groups allowed to unprotect' do + optional :access_level, type: Integer, values: ProtectedBranch::UnprotectAccessLevel.allowed_access_levels + optional :user_id, type: Integer + optional :group_id, type: Integer + end + end end # rubocop: disable CodeReuse/ActiveRecord post ':id/protected_branches' do diff --git a/lib/api/release/links.rb b/lib/api/release/links.rb index e3072684ef7..5d1b40e3bff 100644 --- a/lib/api/release/links.rb +++ b/lib/api/release/links.rb @@ -8,6 +8,8 @@ module API RELEASE_ENDPOINT_REQUIREMETS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS .merge(tag_name: API::NO_SLASH_URL_PART_REGEX) + before { authorize! :read_release, user_project } + params do requires :id, type: String, desc: 'The ID of a project' end diff --git a/lib/api/releases.rb b/lib/api/releases.rb index cb85028f22c..6b17f4317db 100644 --- a/lib/api/releases.rb +++ b/lib/api/releases.rb @@ -23,7 +23,7 @@ module API get ':id/releases' do releases = ::ReleasesFinder.new(user_project, current_user).execute - present paginate(releases), with: Entities::Release + present paginate(releases), with: Entities::Release, current_user: current_user end desc 'Get a single project release' do @@ -34,9 +34,9 @@ module API requires :tag_name, type: String, desc: 'The name of the tag', as: :tag end get ':id/releases/:tag_name', requirements: RELEASE_ENDPOINT_REQUIREMETS do - authorize_read_release! + authorize_download_code! - present release, with: Entities::Release + present release, with: Entities::Release, current_user: current_user end desc 'Create a new release' do @@ -63,7 +63,7 @@ module API .execute if result[:status] == :success - present result[:release], with: Entities::Release + present result[:release], with: Entities::Release, current_user: current_user else render_api_error!(result[:message], result[:http_status]) end @@ -86,7 +86,7 @@ module API .execute if result[:status] == :success - present result[:release], with: Entities::Release + present result[:release], with: Entities::Release, current_user: current_user else render_api_error!(result[:message], result[:http_status]) end @@ -107,7 +107,7 @@ module API .execute if result[:status] == :success - present result[:release], with: Entities::Release + present result[:release], with: Entities::Release, current_user: current_user else render_api_error!(result[:message], result[:http_status]) end @@ -135,6 +135,10 @@ module API authorize! :destroy_release, release end + def authorize_download_code! + authorize! :download_code, release + end + def release @release ||= user_project.releases.find_by_tag(params[:tag]) end diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb index 32e05d84491..4106a2cdf38 100644 --- a/lib/api/repositories.rb +++ b/lib/api/repositories.rb @@ -89,11 +89,9 @@ module API optional :format, type: String, desc: 'The archive format' end get ':id/repository/archive', requirements: { format: Gitlab::PathRegex.archive_formats_regex } do - begin - send_git_archive user_project.repository, ref: params[:sha], format: params[:format], append_sha: true - rescue - not_found!('File') - end + send_git_archive user_project.repository, ref: params[:sha], format: params[:format], append_sha: true + rescue + not_found!('File') end desc 'Compare two branches, tags, or commits' do @@ -118,12 +116,10 @@ module API optional :sort, type: String, values: %w[asc desc], default: 'asc', desc: 'Sort by asc (ascending) or desc (descending)' end get ':id/repository/contributors' do - begin - contributors = ::Kaminari.paginate_array(user_project.repository.contributors(order_by: params[:order_by], sort: params[:sort])) - present paginate(contributors), with: Entities::Contributor - rescue - not_found! - end + contributors = ::Kaminari.paginate_array(user_project.repository.contributors(order_by: params[:order_by], sort: params[:sort])) + present paginate(contributors), with: Entities::Contributor + rescue + not_found! end desc 'Get the common ancestor between commits' do diff --git a/lib/api/resource_label_events.rb b/lib/api/resource_label_events.rb index 0c328f7268e..448bef12cec 100644 --- a/lib/api/resource_label_events.rb +++ b/lib/api/resource_label_events.rb @@ -7,9 +7,7 @@ module API before { authenticate! } - EVENTABLE_TYPES = [Issue, MergeRequest].freeze - - EVENTABLE_TYPES.each do |eventable_type| + Helpers::ResourceLabelEventsHelpers.eventable_types.each do |eventable_type| parent_type = eventable_type.parent_class.to_s.underscore eventables_str = eventable_type.to_s.underscore.pluralize diff --git a/lib/api/runner.rb b/lib/api/runner.rb index c60d25b88cb..ea36c24eca2 100644 --- a/lib/api/runner.rb +++ b/lib/api/runner.rb @@ -15,12 +15,14 @@ module API optional :info, type: Hash, desc: %q(Runner's metadata) optional :active, type: Boolean, desc: 'Should Runner be active' optional :locked, type: Boolean, desc: 'Should Runner be locked for current project' + optional :access_level, type: String, values: Ci::Runner.access_levels.keys, + desc: 'The access_level of the runner' optional :run_untagged, type: Boolean, desc: 'Should Runner handle untagged jobs' optional :tag_list, type: Array[String], desc: %q(List of Runner's tags) optional :maximum_timeout, type: Integer, desc: 'Maximum timeout set when this Runner will handle the job' end post '/' do - attributes = attributes_for_keys([:description, :active, :locked, :run_untagged, :tag_list, :maximum_timeout]) + attributes = attributes_for_keys([:description, :active, :locked, :run_untagged, :tag_list, :access_level, :maximum_timeout]) .merge(get_runner_details_from_request) attributes = diff --git a/lib/api/search.rb b/lib/api/search.rb index f5db692afe5..1cab1a97186 100644 --- a/lib/api/search.rb +++ b/lib/api/search.rb @@ -17,7 +17,8 @@ module API blobs: Entities::Blob, wiki_blobs: Entities::Blob, snippet_titles: Entities::Snippet, - snippet_blobs: Entities::Snippet + snippet_blobs: Entities::Snippet, + users: Entities::UserBasic }.freeze def search(additional_params = {}) @@ -45,6 +46,18 @@ module API def entity SCOPE_ENTITY[params[:scope].to_sym] end + + def verify_search_scope! + # In EE we have additional validation requirements for searches. + # Defining this method here as a noop allows us to easily extend it in + # EE, without having to modify this file directly. + end + + def check_users_search_allowed! + if params[:scope].to_sym == :users && Feature.disabled?(:users_search, default_enabled: true) + render_api_error!({ error: _("Scope not supported with disabled 'users_search' feature!") }, 400) + end + end end resource :search do @@ -55,12 +68,14 @@ module API requires :search, type: String, desc: 'The expression it should be searched for' requires :scope, type: String, - desc: 'The scope of search, available scopes: - projects, issues, merge_requests, milestones, snippet_titles, snippet_blobs', - values: %w(projects issues merge_requests milestones snippet_titles snippet_blobs) + desc: 'The scope of the search', + values: Helpers::SearchHelpers.global_search_scopes use :pagination end get do + verify_search_scope! + check_users_search_allowed! + present search, with: entity end end @@ -74,12 +89,14 @@ module API requires :search, type: String, desc: 'The expression it should be searched for' requires :scope, type: String, - desc: 'The scope of search, available scopes: - projects, issues, merge_requests, milestones', - values: %w(projects issues merge_requests milestones) + desc: 'The scope of the search', + values: Helpers::SearchHelpers.group_search_scopes use :pagination end get ':id/(-/)search' do + verify_search_scope! + check_users_search_allowed! + present search(group_id: user_group.id), with: entity end end @@ -93,13 +110,15 @@ module API requires :search, type: String, desc: 'The expression it should be searched for' requires :scope, type: String, - desc: 'The scope of search, available scopes: - issues, merge_requests, milestones, notes, wiki_blobs, commits, blobs', - values: %w(issues merge_requests milestones notes wiki_blobs commits blobs) + 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' use :pagination end get ':id/(-/)search' do - present search(project_id: user_project.id), with: entity + check_users_search_allowed! + + present search({ project_id: user_project.id, repository_ref: params[:ref] }), with: entity end end end diff --git a/lib/api/services.rb b/lib/api/services.rb index bda6be51553..bc77fae87fa 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -1,696 +1,8 @@ # frozen_string_literal: true module API class Services < Grape::API - CHAT_NOTIFICATION_SETTINGS = [ - { - required: true, - name: :webhook, - type: String, - desc: 'The chat webhook' - }, - { - required: false, - name: :username, - type: String, - desc: 'The chat username' - }, - { - required: false, - name: :channel, - type: String, - desc: 'The default chat channel' - } - ].freeze - - CHAT_NOTIFICATION_FLAGS = [ - { - required: false, - name: :notify_only_broken_pipelines, - type: Boolean, - desc: 'Send notifications for broken pipelines' - }, - { - required: false, - name: :notify_only_default_branch, - type: Boolean, - desc: 'Send notifications only for the default branch' - } - ].freeze - - CHAT_NOTIFICATION_CHANNELS = [ - { - required: false, - name: :push_channel, - type: String, - desc: 'The name of the channel to receive push_events notifications' - }, - { - required: false, - name: :issue_channel, - type: String, - desc: 'The name of the channel to receive issues_events notifications' - }, - { - required: false, - name: :confidential_issue_channel, - type: String, - desc: 'The name of the channel to receive confidential_issues_events notifications' - }, - { - required: false, - name: :merge_request_channel, - type: String, - desc: 'The name of the channel to receive merge_requests_events notifications' - }, - { - required: false, - name: :note_channel, - type: String, - desc: 'The name of the channel to receive note_events notifications' - }, - { - required: false, - name: :tag_push_channel, - type: String, - desc: 'The name of the channel to receive tag_push_events notifications' - }, - { - required: false, - name: :pipeline_channel, - type: String, - desc: 'The name of the channel to receive pipeline_events notifications' - }, - { - required: false, - name: :wiki_page_channel, - type: String, - desc: 'The name of the channel to receive wiki_page_events notifications' - } - ].freeze - - CHAT_NOTIFICATION_EVENTS = [ - { - required: false, - name: :push_events, - type: Boolean, - desc: 'Enable notifications for push_events' - }, - { - required: false, - name: :issues_events, - type: Boolean, - desc: 'Enable notifications for issues_events' - }, - { - required: false, - name: :confidential_issues_events, - type: Boolean, - desc: 'Enable notifications for confidential_issues_events' - }, - { - required: false, - name: :merge_requests_events, - type: Boolean, - desc: 'Enable notifications for merge_requests_events' - }, - { - required: false, - name: :note_events, - type: Boolean, - desc: 'Enable notifications for note_events' - }, - { - required: false, - name: :tag_push_events, - type: Boolean, - desc: 'Enable notifications for tag_push_events' - }, - { - required: false, - name: :pipeline_events, - type: Boolean, - desc: 'Enable notifications for pipeline_events' - }, - { - required: false, - name: :wiki_page_events, - type: Boolean, - desc: 'Enable notifications for wiki_page_events' - } - ].freeze - - services = { - 'asana' => [ - { - required: true, - name: :api_key, - type: String, - desc: 'User API token' - }, - { - required: false, - name: :restrict_to_branch, - type: String, - desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches' - } - ], - 'assembla' => [ - { - required: true, - name: :token, - type: String, - desc: 'The authentication token' - }, - { - required: false, - name: :subdomain, - type: String, - desc: 'Subdomain setting' - } - ], - 'bamboo' => [ - { - required: true, - name: :bamboo_url, - type: String, - desc: 'Bamboo root URL like https://bamboo.example.com' - }, - { - required: true, - name: :build_key, - type: String, - desc: 'Bamboo build plan key like' - }, - { - required: true, - name: :username, - type: String, - desc: 'A user with API access, if applicable' - }, - { - required: true, - name: :password, - type: String, - desc: 'Passord of the user' - } - ], - 'bugzilla' => [ - { - required: true, - name: :new_issue_url, - type: String, - desc: 'New issue URL' - }, - { - required: true, - name: :issues_url, - type: String, - desc: 'Issues URL' - }, - { - required: true, - name: :project_url, - type: String, - desc: 'Project URL' - }, - { - required: false, - name: :description, - type: String, - desc: 'Description' - }, - { - required: false, - name: :title, - type: String, - desc: 'Title' - } - ], - 'buildkite' => [ - { - required: true, - name: :token, - type: String, - desc: 'Buildkite project GitLab token' - }, - { - required: true, - name: :project_url, - type: String, - desc: 'The buildkite project URL' - }, - { - required: false, - name: :enable_ssl_verification, - type: Boolean, - desc: 'Enable SSL verification for communication' - } - ], - 'campfire' => [ - { - required: true, - name: :token, - type: String, - desc: 'Campfire token' - }, - { - required: false, - name: :subdomain, - type: String, - desc: 'Campfire subdomain' - }, - { - required: false, - name: :room, - type: String, - desc: 'Campfire room' - } - ], - 'custom-issue-tracker' => [ - { - required: true, - name: :new_issue_url, - type: String, - desc: 'New issue URL' - }, - { - required: true, - name: :issues_url, - type: String, - desc: 'Issues URL' - }, - { - required: true, - name: :project_url, - type: String, - desc: 'Project URL' - }, - { - required: false, - name: :description, - type: String, - desc: 'Description' - }, - { - required: false, - name: :title, - type: String, - desc: 'Title' - } - ], - 'discord' => [ - { - required: true, - name: :webhook, - type: String, - desc: 'Discord webhook. e.g. https://discordapp.com/api/webhooks/…' - } - ], - 'drone-ci' => [ - { - required: true, - name: :token, - type: String, - desc: 'Drone CI token' - }, - { - required: true, - name: :drone_url, - type: String, - desc: 'Drone CI URL' - }, - { - required: false, - name: :enable_ssl_verification, - type: Boolean, - desc: 'Enable SSL verification for communication' - } - ], - 'emails-on-push' => [ - { - required: true, - name: :recipients, - type: String, - desc: 'Comma-separated list of recipient email addresses' - }, - { - required: false, - name: :disable_diffs, - type: Boolean, - desc: 'Disable code diffs' - }, - { - required: false, - name: :send_from_committer_email, - type: Boolean, - desc: 'Send from committer' - } - ], - 'external-wiki' => [ - { - required: true, - name: :external_wiki_url, - type: String, - desc: 'The URL of the external Wiki' - } - ], - 'flowdock' => [ - { - required: true, - name: :token, - type: String, - desc: 'Flowdock token' - } - ], - 'hangouts-chat' => [ - { - required: true, - name: :webhook, - type: String, - desc: 'The Hangouts Chat webhook. e.g. https://chat.googleapis.com/v1/spaces…' - }, - CHAT_NOTIFICATION_EVENTS - ].flatten, - 'irker' => [ - { - required: true, - name: :recipients, - type: String, - desc: 'Recipients/channels separated by whitespaces' - }, - { - required: false, - name: :default_irc_uri, - type: String, - desc: 'Default: irc://irc.network.net:6697' - }, - { - required: false, - name: :server_host, - type: String, - desc: 'Server host. Default localhost' - }, - { - required: false, - name: :server_port, - type: Integer, - desc: 'Server port. Default 6659' - }, - { - required: false, - name: :colorize_messages, - type: Boolean, - desc: 'Colorize messages' - } - ], - 'jira' => [ - { - required: true, - name: :url, - type: String, - desc: 'The base URL to the JIRA instance web interface which is being linked to this GitLab project. E.g., https://jira.example.com' - }, - { - required: false, - name: :api_url, - type: String, - desc: 'The base URL to the JIRA instance API. Web URL value will be used if not set. E.g., https://jira-api.example.com' - }, - { - required: true, - name: :username, - type: String, - desc: 'The username of the user created to be used with GitLab/JIRA' - }, - { - required: true, - name: :password, - type: String, - desc: 'The password of the user created to be used with GitLab/JIRA' - }, - { - required: false, - name: :jira_issue_transition_id, - type: String, - desc: 'The ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`' - } - ], - - 'kubernetes' => [ - { - required: true, - name: :namespace, - type: String, - desc: 'The Kubernetes namespace to use' - }, - { - required: true, - name: :api_url, - type: String, - desc: 'The URL to the Kubernetes cluster API, e.g., https://kubernetes.example.com' - }, - { - required: true, - name: :token, - type: String, - desc: 'The service token to authenticate against the Kubernetes cluster with' - }, - { - required: false, - name: :ca_pem, - type: String, - desc: 'A custom certificate authority bundle to verify the Kubernetes cluster with (PEM format)' - } - ], - 'mattermost-slash-commands' => [ - { - required: true, - name: :token, - type: String, - desc: 'The Mattermost token' - } - ], - 'slack-slash-commands' => [ - { - required: true, - name: :token, - type: String, - desc: 'The Slack token' - } - ], - 'packagist' => [ - { - required: true, - name: :username, - type: String, - desc: 'The username' - }, - { - required: true, - name: :token, - type: String, - desc: 'The Packagist API token' - }, - { - required: false, - name: :server, - type: String, - desc: 'The server' - } - ], - 'pipelines-email' => [ - { - required: true, - name: :recipients, - type: String, - desc: 'Comma-separated list of recipient email addresses' - }, - { - required: false, - name: :notify_only_broken_pipelines, - type: Boolean, - desc: 'Notify only broken pipelines' - } - ], - 'pivotaltracker' => [ - { - required: true, - name: :token, - type: String, - desc: 'The Pivotaltracker token' - }, - { - required: false, - name: :restrict_to_branch, - type: String, - desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches.' - } - ], - 'prometheus' => [ - { - required: true, - name: :api_url, - type: String, - desc: 'Prometheus API Base URL, like http://prometheus.example.com/' - } - ], - 'pushover' => [ - { - required: true, - name: :api_key, - type: String, - desc: 'The application key' - }, - { - required: true, - name: :user_key, - type: String, - desc: 'The user key' - }, - { - required: true, - name: :priority, - type: String, - desc: 'The priority' - }, - { - required: true, - name: :device, - type: String, - desc: 'Leave blank for all active devices' - }, - { - required: true, - name: :sound, - type: String, - desc: 'The sound of the notification' - } - ], - 'redmine' => [ - { - required: true, - name: :new_issue_url, - type: String, - desc: 'The new issue URL' - }, - { - required: true, - name: :project_url, - type: String, - desc: 'The project URL' - }, - { - required: true, - name: :issues_url, - type: String, - desc: 'The issues URL' - }, - { - required: false, - name: :description, - type: String, - desc: 'The description of the tracker' - } - ], - 'youtrack' => [ - { - required: true, - name: :project_url, - type: String, - desc: 'The project URL' - }, - { - required: true, - name: :issues_url, - type: String, - desc: 'The issues URL' - }, - { - required: false, - name: :description, - type: String, - desc: 'The description of the tracker' - } - ], - 'slack' => [ - CHAT_NOTIFICATION_SETTINGS, - CHAT_NOTIFICATION_FLAGS, - CHAT_NOTIFICATION_CHANNELS, - CHAT_NOTIFICATION_EVENTS - ].flatten, - 'microsoft-teams' => [ - { - required: true, - name: :webhook, - type: String, - desc: 'The Microsoft Teams webhook. e.g. https://outlook.office.com/webhook/…' - } - ], - 'mattermost' => [ - CHAT_NOTIFICATION_SETTINGS, - CHAT_NOTIFICATION_FLAGS, - CHAT_NOTIFICATION_CHANNELS, - CHAT_NOTIFICATION_EVENTS - ].flatten, - 'teamcity' => [ - { - required: true, - name: :teamcity_url, - type: String, - desc: 'TeamCity root URL like https://teamcity.example.com' - }, - { - required: true, - name: :build_type, - type: String, - desc: 'Build configuration ID' - }, - { - required: true, - name: :username, - type: String, - desc: 'A user with permissions to trigger a manual build' - }, - { - required: true, - name: :password, - type: String, - desc: 'The password of the user' - } - ] - } - - service_classes = [ - AsanaService, - AssemblaService, - BambooService, - BugzillaService, - BuildkiteService, - CampfireService, - CustomIssueTrackerService, - DiscordService, - DroneCiService, - EmailsOnPushService, - ExternalWikiService, - FlowdockService, - HangoutsChatService, - IrkerService, - JiraService, - KubernetesService, - MattermostSlashCommandsService, - SlackSlashCommandsService, - PackagistService, - PipelinesEmailService, - PivotaltrackerService, - PrometheusService, - PushoverService, - RedmineService, - YoutrackService, - SlackService, - MattermostService, - MicrosoftTeamsService, - TeamcityService - ] + services = Helpers::ServicesHelpers.services + service_classes = Helpers::ServicesHelpers.service_classes if Rails.env.development? services['mock-ci'] = [ @@ -704,11 +16,7 @@ module API services['mock-deployment'] = [] services['mock-monitoring'] = [] - service_classes += [ - MockCiService, - MockDeploymentService, - MockMonitoringService - ] + service_classes += Helpers::ServicesHelpers.development_service_classes end SERVICES = services.freeze diff --git a/lib/api/settings.rb b/lib/api/settings.rb index b16faffe335..8046acfa397 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -9,6 +9,11 @@ module API @current_setting ||= (ApplicationSetting.current_without_cache || ApplicationSetting.create_from_defaults) end + + def filter_attributes_using_license(attrs) + # This method will be redefined in EE. + attrs + end end desc 'Get the current application settings' do @@ -35,7 +40,8 @@ module API end optional :container_registry_token_expire_delay, type: Integer, desc: 'Authorization token duration (minutes)' optional :default_artifacts_expire_in, type: String, desc: "Set the default expiration time for each job's artifacts" - optional :default_branch_protection, type: Integer, values: Gitlab::Access.protection_values, desc: 'Determine if developers can push to master' + optional :default_project_creation, type: Integer, values: ::Gitlab::Access.project_creation_values, desc: 'Determine if developers can create projects in the group' + optional :default_branch_protection, type: Integer, values: ::Gitlab::Access.protection_values, desc: 'Determine if developers can push to master' optional :default_group_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default group visibility' optional :default_project_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default project visibility' optional :default_projects_limit, type: Integer, desc: 'The maximum number of personal projects' @@ -130,7 +136,51 @@ module API desc: "Restrictions on the complexity of uploaded #{type.upcase} keys. A value of #{ApplicationSetting::FORBIDDEN_KEY_VALUE} disables all #{type.upcase} keys." end - optional_attributes = ::ApplicationSettingsHelper.visible_attributes << :performance_bar_allowed_group_id + if Gitlab.ee? + optional :elasticsearch_aws, type: Boolean, desc: 'Enable support for AWS hosted elasticsearch' + + given elasticsearch_aws: ->(val) { val } do + optional :elasticsearch_aws_access_key, type: String, desc: 'AWS IAM access key' + requires :elasticsearch_aws_region, type: String, desc: 'The AWS region the elasticsearch domain is configured' + optional :elasticsearch_aws_secret_access_key, type: String, desc: 'AWS IAM secret access key' + end + + optional :elasticsearch_indexing, type: Boolean, desc: 'Enable Elasticsearch indexing' + + given elasticsearch_indexing: ->(val) { val } do + optional :elasticsearch_search, type: Boolean, desc: 'Enable Elasticsearch search' + requires :elasticsearch_url, type: String, desc: 'The url to use for connecting to Elasticsearch. Use a comma-separated list to support clustering (e.g., "http://localhost:9200, http://localhost:9201")' + optional :elasticsearch_limit_indexing, type: Boolean, desc: 'Limit Elasticsearch to index certain namespaces and projects' + end + + given elasticsearch_limit_indexing: ->(val) { val } do + optional :elasticsearch_namespace_ids, type: Array[Integer], coerce_with: Validations::Types::LabelsList.coerce, desc: 'The namespace ids to index with Elasticsearch.' + optional :elasticsearch_project_ids, type: Array[Integer], coerce_with: Validations::Types::LabelsList.coerce, desc: 'The project ids to index with Elasticsearch.' + end + + optional :email_additional_text, type: String, desc: 'Additional text added to the bottom of every email for legal/auditing/compliance reasons' + optional :help_text, type: String, desc: 'GitLab server administrator information' + optional :repository_size_limit, type: Integer, desc: 'Size limit per repository (MB)' + optional :file_template_project_id, type: Integer, desc: 'ID of project where instance-level file templates are stored.' + optional :repository_storages, type: Array[String], desc: 'A list of names of enabled storage paths, taken from `gitlab.yml`. New projects will be created in one of these stores, chosen at random.' + optional :snowplow_enabled, type: Boolean, desc: 'Enable Snowplow' + + given snowplow_enabled: ->(val) { val } do + requires :snowplow_collector_uri, type: String, desc: 'Snowplow Collector URI' + optional :snowplow_cookie_domain, type: String, desc: 'Snowplow cookie domain' + optional :snowplow_site_id, type: String, desc: 'Snowplow Site/Application ID' + end + + optional :usage_ping_enabled, type: Boolean, desc: 'Every week GitLab will report license usage back to GitLab, Inc.' + end + + optional_attributes = [*::ApplicationSettingsHelper.visible_attributes, + *::ApplicationSettingsHelper.external_authorization_service_attributes, + :performance_bar_allowed_group_id] + + if Gitlab.ee? + optional_attributes += EE::ApplicationSettingsHelper.possible_licensed_attributes + end optional(*optional_attributes) at_least_one_of(*optional_attributes) @@ -156,6 +206,8 @@ module API attrs[:password_authentication_enabled_for_web] = attrs.delete(:password_authentication_enabled) end + attrs = filter_attributes_using_license(attrs) + if ApplicationSettings::UpdateService.new(current_settings, current_user, attrs).execute present current_settings, with: Entities::ApplicationSetting else diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb index 326d55afd0e..f8b37b33348 100644 --- a/lib/api/snippets.rb +++ b/lib/api/snippets.rb @@ -16,6 +16,10 @@ module API def public_snippets SnippetsFinder.new(current_user, scope: :are_public).execute end + + def snippets + SnippetsFinder.new(current_user).execute + end end desc 'Get a snippets list for authenticated user' do @@ -48,7 +52,10 @@ module API requires :id, type: Integer, desc: 'The ID of a snippet' end get ':id' do - snippet = snippets_for_current_user.find(params[:id]) + snippet = snippets.find_by_id(params[:id]) + + break not_found!('Snippet') unless snippet + present snippet, with: Entities::PersonalSnippet end @@ -94,9 +101,8 @@ module API desc: 'The visibility of the snippet' at_least_one_of :title, :file_name, :content, :visibility end - # rubocop: disable CodeReuse/ActiveRecord put ':id' do - snippet = snippets_for_current_user.find_by(id: params.delete(:id)) + snippet = snippets_for_current_user.find_by_id(params.delete(:id)) break not_found!('Snippet') unless snippet authorize! :update_personal_snippet, snippet @@ -113,7 +119,6 @@ module API render_validation_error!(snippet) end end - # rubocop: enable CodeReuse/ActiveRecord desc 'Remove snippet' do detail 'This feature was introduced in GitLab 8.15.' @@ -122,16 +127,14 @@ module API params do requires :id, type: Integer, desc: 'The ID of a snippet' end - # rubocop: disable CodeReuse/ActiveRecord delete ':id' do - snippet = snippets_for_current_user.find_by(id: params.delete(:id)) + snippet = snippets_for_current_user.find_by_id(params.delete(:id)) break not_found!('Snippet') unless snippet authorize! :destroy_personal_snippet, snippet destroy_conditionally!(snippet) end - # rubocop: enable CodeReuse/ActiveRecord desc 'Get a raw snippet' do detail 'This feature was introduced in GitLab 8.15.' @@ -139,9 +142,8 @@ module API params do requires :id, type: Integer, desc: 'The ID of a snippet' end - # rubocop: disable CodeReuse/ActiveRecord get ":id/raw" do - snippet = snippets_for_current_user.find_by(id: params.delete(:id)) + snippet = snippets.find_by_id(params.delete(:id)) break not_found!('Snippet') unless snippet env['api.format'] = :txt @@ -149,7 +151,6 @@ module API header['Content-Disposition'] = 'attachment' present snippet.content end - # rubocop: enable CodeReuse/ActiveRecord desc 'Get the user agent details for a snippet' do success Entities::UserAgentDetail @@ -157,17 +158,15 @@ module API params do requires :id, type: Integer, desc: 'The ID of a snippet' end - # rubocop: disable CodeReuse/ActiveRecord get ":id/user_agent_detail" do authenticated_as_admin! - snippet = Snippet.find_by!(id: params[:id]) + snippet = Snippet.find_by_id!(params[:id]) break not_found!('UserAgentDetail') unless snippet.user_agent_detail present snippet.user_agent_detail, with: Entities::UserAgentDetail end - # rubocop: enable CodeReuse/ActiveRecord end end end diff --git a/lib/api/todos.rb b/lib/api/todos.rb index 64ac8ece56c..d2196f05173 100644 --- a/lib/api/todos.rb +++ b/lib/api/todos.rb @@ -6,6 +6,8 @@ module API before { authenticate! } + helpers ::Gitlab::IssuableMetadata + ISSUABLE_TYPES = { 'merge_requests' => ->(iid) { find_merge_request_with_access(iid) }, 'issues' => ->(iid) { find_project_issue(iid) } @@ -42,6 +44,30 @@ module API def find_todos TodosFinder.new(current_user, params).execute end + + def issuable_and_awardable?(type) + obj_type = Object.const_get(type) + + (obj_type < Issuable) && (obj_type < Awardable) + rescue NameError + false + end + + def batch_load_issuable_metadata(todos, options) + # This should be paginated and will cause Rails to SELECT for all the Todos + todos_by_type = todos.group_by(&:target_type) + + todos_by_type.keys.each do |type| + next unless issuable_and_awardable?(type) + + collection = todos_by_type[type] + + next unless collection + + targets = collection.map(&:target) + options[type] = { issuable_metadata: issuable_meta_data(targets, type) } + end + end end desc 'Get a todo list' do @@ -51,7 +77,11 @@ module API use :pagination end get do - present paginate(find_todos), with: Entities::Todo, current_user: current_user + todos = paginate(find_todos.with_api_entity_associations) + options = { with: Entities::Todo, current_user: current_user } + batch_load_issuable_metadata(todos, options) + + present todos, options end desc 'Mark a todo as done' do diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb index 8fc7c7361e1..0e829c5699b 100644 --- a/lib/api/triggers.rb +++ b/lib/api/triggers.rb @@ -13,7 +13,7 @@ module API end params do requires :ref, type: String, desc: 'The commit sha or name of a branch or tag', allow_blank: false - requires :token, type: String, desc: 'The unique token of trigger' + requires :token, type: String, desc: 'The unique token of trigger or job token' optional :variables, type: Hash, desc: 'The list of variables to be injected into build' end post ":id/(ref/:ref/)trigger/pipeline", requirements: { ref: /.+/ } do diff --git a/lib/api/users.rb b/lib/api/users.rb index 7d88880d412..2f23e33bd4a 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -51,6 +51,11 @@ module API optional :avatar, type: File, desc: 'Avatar image for user' optional :private_profile, type: Boolean, desc: 'Flag indicating the user has a private profile' all_or_none_of :extern_uid, :provider + + if Gitlab.ee? + optional :shared_runners_minutes_limit, type: Integer, desc: 'Pipeline minutes quota for this user' + optional :extra_shared_runners_minutes_limit, type: Integer, desc: '(admin-only) Extra pipeline minutes quota for this user' + end end params :sort_params do @@ -80,6 +85,10 @@ module API use :sort_params use :pagination use :with_custom_attributes + + if Gitlab.ee? + optional :skip_ldap, type: Boolean, default: false, desc: 'Skip LDAP users' + end end # rubocop: disable CodeReuse/ActiveRecord get do @@ -124,7 +133,7 @@ module API user = User.find_by(id: params[:id]) not_found!('User') unless user && can?(current_user, :read_user, user) - opts = { with: current_user&.admin? ? Entities::UserWithAdmin : Entities::User, current_user: current_user } + opts = { with: current_user&.admin? ? Entities::UserDetailsWithAdmin : Entities::User, current_user: current_user } user, opts = with_custom_attributes(user, opts) present user, opts diff --git a/lib/api/validations/types/labels_list.rb b/lib/api/validations/types/labels_list.rb new file mode 100644 index 00000000000..47cd83c29cf --- /dev/null +++ b/lib/api/validations/types/labels_list.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module API + module Validations + module Types + class LabelsList + def self.coerce + lambda do |value| + case value + when String + value.split(',').map(&:strip) + when Array + value.map { |v| v.to_s.split(',').map(&:strip) }.flatten + when LabelsList + value + else + [] + end + end + end + end + end + end +end diff --git a/lib/api/variables.rb b/lib/api/variables.rb index 148deb86c4c..a1bb21b3a06 100644 --- a/lib/api/variables.rb +++ b/lib/api/variables.rb @@ -7,6 +7,14 @@ module API before { authenticate! } before { authorize! :admin_build, user_project } + helpers do + def filter_variable_parameters(params) + # This method exists so that EE can more easily filter out certain + # parameters, without having to modify the source code directly. + params + end + end + params do requires :id, type: String, desc: 'The ID of a project' end @@ -47,9 +55,15 @@ module API requires :key, type: String, desc: 'The key of the variable' requires :value, type: String, desc: 'The value of the variable' optional :protected, type: String, desc: 'Whether the variable is protected' + optional :variable_type, type: String, values: Ci::Variable.variable_types.keys, desc: 'The type of variable, must be one of env_var or file. Defaults to env_var' + + if Gitlab.ee? + optional :environment_scope, type: String, desc: 'The environment_scope of the variable' + end end post ':id/variables' do variable_params = declared_params(include_missing: false) + variable_params = filter_variable_parameters(variable_params) variable = user_project.variables.create(variable_params) @@ -67,6 +81,11 @@ module API optional :key, type: String, desc: 'The key of the variable' optional :value, type: String, desc: 'The value of the variable' optional :protected, type: String, desc: 'Whether the variable is protected' + optional :variable_type, type: String, values: Ci::Variable.variable_types.keys, desc: 'The type of variable, must be one of env_var or file' + + if Gitlab.ee? + optional :environment_scope, type: String, desc: 'The environment_scope of the variable' + end end # rubocop: disable CodeReuse/ActiveRecord put ':id/variables/:key' do @@ -75,6 +94,7 @@ module API break not_found!('Variable') unless variable variable_params = declared_params(include_missing: false).except(:key) + variable_params = filter_variable_parameters(variable_params) if variable.update(variable_params) present variable, with: Entities::Variable diff --git a/lib/api/version.rb b/lib/api/version.rb index 74cd857f447..eca1b529094 100644 --- a/lib/api/version.rb +++ b/lib/api/version.rb @@ -2,13 +2,29 @@ module API class Version < Grape::API + helpers ::API::Helpers::GraphqlHelpers + before { authenticate! } + METADATA_QUERY = <<~EOF + { + metadata { + version + revision + } + } + EOF + desc 'Get the version information of the GitLab instance.' do detail 'This feature was introduced in GitLab 8.13.' end get '/version' do - { version: Gitlab::VERSION, revision: Gitlab.revision } + conditionally_graphql!( + query: METADATA_QUERY, + context: { current_user: current_user }, + transform: ->(result) { result.dig('data', 'metadata') }, + fallback: -> { { version: Gitlab::VERSION, revision: Gitlab.revision } } + ) end end end diff --git a/lib/api/wikis.rb b/lib/api/wikis.rb index 994074ddc67..5724adb2c40 100644 --- a/lib/api/wikis.rb +++ b/lib/api/wikis.rb @@ -33,7 +33,8 @@ module API authorize! :read_wiki, user_project entity = params[:with_content] ? Entities::WikiPage : Entities::WikiPageBasic - present user_project.wiki.pages, with: entity + + present user_project.wiki.list_pages(load_content: params[:with_content]), with: entity end desc 'Get a wiki page' do |