diff options
author | Grzegorz Bizon <grzesiek.bizon@gmail.com> | 2017-03-21 14:22:56 +0100 |
---|---|---|
committer | Grzegorz Bizon <grzesiek.bizon@gmail.com> | 2017-03-21 14:22:56 +0100 |
commit | c5912ecd73560b730eda625c77d900ca23ab16d5 (patch) | |
tree | 8f7288b6209fb7e542e5d3bf867138ea6bde7faf /lib/api | |
parent | 53d332d3c73f8a883fa54d8eaaf91f92da73c33f (diff) | |
parent | 1e5888d115df1973cd5af0aa95013dbbf29ddefd (diff) | |
download | gitlab-ce-c5912ecd73560b730eda625c77d900ca23ab16d5.tar.gz |
Merge branch 'master' into feature/multi-level-container-registry-images
* master: (1327 commits)
Merge branch 'render-json-leak' into 'security'
Merge branch 'ssrf' into 'security'
Merge branch 'ssrf' into 'security'
Merge branch 'fix-links-target-blank' into 'security'
Merge branch '28058-hide-emails-in-atom-feeds' into 'security'
Fix karma test
Reset filters after click
Handle Route#name being nil after an update
Only add frontend code coverage instrumentation when generating coverage report
fix recompile assets step in 9.0 upgrade guide to use yarn
Undo explicit conversion to Integer
Make level_value accept string integers
Make feature spec more robust
Removed d3.js from the main application.js bundle
Extend compound status for manual actions specs
Update css to be nice and tidy.
Fix pipeline status for transition between stages
add an index to the ghost column
Return 404 in project issues API endpoint when project cannot be found
Improve rename projects migration
...
Conflicts:
doc/ci/docker/using_docker_build.md
spec/lib/gitlab/import_export/all_models.yml
Diffstat (limited to 'lib/api')
83 files changed, 4078 insertions, 617 deletions
diff --git a/lib/api/access_requests.rb b/lib/api/access_requests.rb index 789f45489eb..a5c9f0b509c 100644 --- a/lib/api/access_requests.rb +++ b/lib/api/access_requests.rb @@ -10,7 +10,7 @@ module API params do requires :id, type: String, desc: "The #{source_type} ID" end - resource source_type.pluralize do + resource source_type.pluralize, requirements: { id: %r{[^/]+} } do desc "Gets a list of access requests for a #{source_type}." do detail 'This feature was introduced in GitLab 8.11.' success Entities::AccessRequester diff --git a/lib/api/api.rb b/lib/api/api.rb index ed775f898d2..7c7bfada7d0 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -5,26 +5,42 @@ module API version %w(v3 v4), using: :path version 'v3', using: :path do + helpers ::API::V3::Helpers + + mount ::API::V3::AwardEmoji mount ::API::V3::Boards mount ::API::V3::Branches + mount ::API::V3::BroadcastMessages + mount ::API::V3::Builds mount ::API::V3::Commits mount ::API::V3::DeployKeys + mount ::API::V3::Environments mount ::API::V3::Files + mount ::API::V3::Groups mount ::API::V3::Issues mount ::API::V3::Labels mount ::API::V3::Members mount ::API::V3::MergeRequestDiffs mount ::API::V3::MergeRequests + mount ::API::V3::Notes + mount ::API::V3::Pipelines mount ::API::V3::ProjectHooks + mount ::API::V3::Milestones mount ::API::V3::Projects mount ::API::V3::ProjectSnippets mount ::API::V3::Repositories + mount ::API::V3::Runners + mount ::API::V3::Services + mount ::API::V3::Settings + mount ::API::V3::Snippets mount ::API::V3::Subscriptions mount ::API::V3::SystemHooks mount ::API::V3::Tags - mount ::API::V3::Todos mount ::API::V3::Templates + mount ::API::V3::Todos + mount ::API::V3::Triggers mount ::API::V3::Users + mount ::API::V3::Variables end before { allow_access_with_scope :api } @@ -47,6 +63,10 @@ module API error! e.message, e.status, e.headers end + rescue_from Gitlab::Auth::TooManyIps do |e| + rack_response({ 'message' => '403 Forbidden' }.to_json, 403) + end + rescue_from :all do |exception| handle_api_exception(exception) end @@ -64,7 +84,6 @@ module API mount ::API::Boards mount ::API::Branches mount ::API::BroadcastMessages - mount ::API::Builds mount ::API::Commits mount ::API::CommitStatuses mount ::API::DeployKeys @@ -74,6 +93,7 @@ module API mount ::API::Groups mount ::API::Internal mount ::API::Issues + mount ::API::Jobs mount ::API::Keys mount ::API::Labels mount ::API::Lint @@ -90,6 +110,7 @@ module API mount ::API::Projects mount ::API::ProjectSnippets mount ::API::Repositories + mount ::API::Runner mount ::API::Runners mount ::API::Services mount ::API::Session diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb index df6db140d0e..409cb5b924f 100644 --- a/lib/api/api_guard.rb +++ b/lib/api/api_guard.rb @@ -6,7 +6,7 @@ module API module APIGuard extend ActiveSupport::Concern - PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN" + PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN".freeze PRIVATE_TOKEN_PARAM = :private_token included do |base| @@ -114,8 +114,8 @@ module API private def install_error_responders(base) - error_classes = [ MissingTokenError, TokenNotFoundError, - ExpiredError, RevokedError, InsufficientScopeError] + error_classes = [MissingTokenError, TokenNotFoundError, + ExpiredError, RevokedError, InsufficientScopeError] base.send :rescue_from, *error_classes, oauth2_bearer_token_error_handler end @@ -160,13 +160,10 @@ module API # Exceptions # - class MissingTokenError < StandardError; end - - class TokenNotFoundError < StandardError; end - - class ExpiredError < StandardError; end - - class RevokedError < StandardError; end + MissingTokenError = Class.new(StandardError) + TokenNotFoundError = Class.new(StandardError) + ExpiredError = Class.new(StandardError) + RevokedError = Class.new(StandardError) class InsufficientScopeError < StandardError attr_reader :scopes diff --git a/lib/api/award_emoji.rb b/lib/api/award_emoji.rb index 2ef327217ea..56f19f89642 100644 --- a/lib/api/award_emoji.rb +++ b/lib/api/award_emoji.rb @@ -3,19 +3,26 @@ module API include PaginationParams before { authenticate! } - AWARDABLES = %w[issue merge_request snippet] - - resource :projects do - AWARDABLES.each do |awardable_type| - awardable_string = awardable_type.pluralize - awardable_id_string = "#{awardable_type}_id" + AWARDABLES = [ + { type: 'issue', find_by: :iid }, + { type: 'merge_request', find_by: :iid }, + { type: 'snippet', find_by: :id } + ].freeze + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects, requirements: { id: %r{[^/]+} } do + AWARDABLES.each do |awardable_params| + awardable_string = awardable_params[:type].pluralize + awardable_id_string = "#{awardable_params[:type]}_#{awardable_params[:find_by]}" params do - requires :id, type: String, desc: 'The ID of a project' requires :"#{awardable_id_string}", type: Integer, desc: "The ID of an Issue, Merge Request or Snippet" end - [ ":id/#{awardable_string}/:#{awardable_id_string}/award_emoji", + [ + ":id/#{awardable_string}/:#{awardable_id_string}/award_emoji", ":id/#{awardable_string}/:#{awardable_id_string}/notes/:note_id/award_emoji" ].each do |endpoint| @@ -82,7 +89,6 @@ module API unauthorized! unless award.user == current_user || current_user.admin? award.destroy - present award, with: Entities::AwardEmoji end end end @@ -104,10 +110,10 @@ module API note_id = params.delete(:note_id) awardable.notes.find(note_id) - elsif params.include?(:issue_id) - user_project.issues.find(params[:issue_id]) - elsif params.include?(:merge_request_id) - user_project.merge_requests.find(params[:merge_request_id]) + elsif params.include?(:issue_iid) + user_project.issues.find_by!(iid: params[:issue_iid]) + elsif params.include?(:merge_request_iid) + user_project.merge_requests.find_by!(iid: params[:merge_request_iid]) else user_project.snippets.find(params[:snippet_id]) end diff --git a/lib/api/boards.rb b/lib/api/boards.rb index f4226e5a89d..5a2d7a681e3 100644 --- a/lib/api/boards.rb +++ b/lib/api/boards.rb @@ -7,7 +7,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects do + resource :projects, requirements: { id: %r{[^/]+} } do desc 'Get all project boards' do detail 'This feature was introduced in 8.13' success Entities::Board @@ -127,9 +127,7 @@ module API service = ::Boards::Lists::DestroyService.new(user_project, current_user) - if service.execute(list) - present list, with: Entities::List - else + unless service.execute(list) render_api_error!({ error: 'List could not be deleted!' }, 400) end end diff --git a/lib/api/branches.rb b/lib/api/branches.rb index c65de90cca2..f35084a582a 100644 --- a/lib/api/branches.rb +++ b/lib/api/branches.rb @@ -4,13 +4,12 @@ module API class Branches < Grape::API include PaginationParams - before { authenticate! } before { authorize! :download_code, user_project } params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects do + resource :projects, requirements: { id: %r{[^/]+} } do desc 'Get a project repository branches' do success Entities::RepoBranch end @@ -102,6 +101,7 @@ module API end post ":id/repository/branches" do authorize_push_project + result = CreateBranchService.new(user_project, current_user). execute(params[:branch], params[:ref]) @@ -124,11 +124,7 @@ module API result = DeleteBranchService.new(user_project, current_user). execute(params[:branch]) - if result[:status] == :success - { - branch: params[:branch] - } - else + if result[:status] != :success render_api_error!(result[:message], result[:return_code]) end end @@ -137,7 +133,7 @@ module API delete ":id/repository/merged_branches" do DeleteMergedBranchesService.new(user_project, current_user).async_execute - status(200) + accepted! end end end diff --git a/lib/api/broadcast_messages.rb b/lib/api/broadcast_messages.rb index 1217002bf8e..395c401203c 100644 --- a/lib/api/broadcast_messages.rb +++ b/lib/api/broadcast_messages.rb @@ -91,7 +91,7 @@ module API delete ':id' do message = find_message - present message.destroy, with: Entities::BroadcastMessage + message.destroy end end end diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb index 0b6076bd28c..827a38d33da 100644 --- a/lib/api/commit_statuses.rb +++ b/lib/api/commit_statuses.rb @@ -2,7 +2,10 @@ require 'mime/types' module API class CommitStatuses < Grape::API - resource :projects do + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects, requirements: { id: %r{[^/]+} } do include PaginationParams before { authenticate! } @@ -11,7 +14,6 @@ module API success Entities::CommitStatus end params do - requires :id, type: String, desc: 'The ID of a project' requires :sha, type: String, desc: 'The commit hash' optional :ref, type: String, desc: 'The ref' optional :stage, type: String, desc: 'The stage' @@ -37,10 +39,9 @@ module API success Entities::CommitStatus end params do - requires :id, type: String, desc: 'The ID of a project' requires :sha, type: String, desc: 'The commit hash' requires :state, type: String, desc: 'The state of the status', - values: ['pending', 'running', 'success', 'failed', 'canceled'] + values: %w(pending running success failed canceled) optional :ref, type: String, desc: 'The ref' optional :target_url, type: String, desc: 'The target URL to associate with this status' optional :description, type: String, desc: 'A short description of the status' @@ -72,14 +73,15 @@ module API status = GenericCommitStatus.running_or_pending.find_or_initialize_by( project: @project, pipeline: pipeline, - user: current_user, name: name, ref: ref, - target_url: params[:target_url], - description: params[:description], - coverage: params[:coverage] + user: current_user ) + optional_attributes = + attributes_for_keys(%w[target_url description coverage]) + + status.update(optional_attributes) if optional_attributes.any? render_validation_error!(status) if status.invalid? begin diff --git a/lib/api/commits.rb b/lib/api/commits.rb index 0cd817f9352..66b37fd2bcc 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -10,7 +10,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects do + resource :projects, requirements: { id: %r{[^/]+} } do desc 'Get a project repository commits' do success Entities::RepoCommit end @@ -18,22 +18,34 @@ module API optional :ref_name, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used' optional :since, type: DateTime, desc: 'Only commits after or on this date will be returned' optional :until, type: DateTime, desc: 'Only commits before or on this date will be returned' - optional :page, type: Integer, default: 0, desc: 'The page for pagination' - optional :per_page, type: Integer, default: 20, desc: 'The number of results per page' optional :path, type: String, desc: 'The file path' + use :pagination end get ":id/repository/commits" do - ref = params[:ref_name] || user_project.try(:default_branch) || 'master' - offset = params[:page] * params[:per_page] + path = params[:path] + before = params[:until] + after = params[:since] + ref = params[:ref_name] || user_project.try(:default_branch) || 'master' + offset = (params[:page] - 1) * params[:per_page] commits = user_project.repository.commits(ref, - path: params[:path], + path: path, limit: params[:per_page], offset: offset, - after: params[:since], - before: params[:until]) + before: before, + after: after) + + commit_count = + if path || before || after + user_project.repository.count_commits(ref: ref, path: path, before: before, after: after) + else + # Cacheable commit count. + user_project.repository.commit_count_for_ref(ref) + end + + paginated_commits = Kaminari.paginate_array(commits, total_count: commit_count) - present commits, with: Entities::RepoCommit + present paginate(paginated_commits), with: Entities::RepoCommit end desc 'Commit multiple file changes as one commit' do @@ -52,13 +64,6 @@ module API attrs = declared_params.merge(start_branch: declared_params[:branch], target_branch: declared_params[:branch]) - attrs[:actions].map! do |action| - action[:action] = action[:action].to_sym - action[:file_path].slice!(0) if action[:file_path] && action[:file_path].start_with?('/') - action[:previous_path].slice!(0) if action[:previous_path] && action[:previous_path].start_with?('/') - action - end - result = ::Files::MultiService.new(user_project, current_user, attrs).execute if result[:status] == :success @@ -134,7 +139,7 @@ module API commit_params = { commit: commit, - create_merge_request: false, + start_branch: params[:branch], target_branch: params[:branch] } @@ -157,7 +162,7 @@ module API optional :path, type: String, desc: 'The file path' given :path do requires :line, type: Integer, desc: 'The line number' - requires :line_type, type: String, values: ['new', 'old'], default: 'new', desc: 'The type of the line' + requires :line_type, type: String, values: %w(new old), default: 'new', desc: 'The type of the line' end end post ':id/repository/commits/:sha/comments' do diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb index 69e85c27a65..b888ede6fe8 100644 --- a/lib/api/deploy_keys.rb +++ b/lib/api/deploy_keys.rb @@ -17,7 +17,7 @@ module API params do requires :id, type: String, desc: 'The ID of the project' end - resource :projects do + resource :projects, requirements: { id: %r{[^/]+} } do before { authorize_admin_project } desc "Get a specific project's deploy keys" do diff --git a/lib/api/deployments.rb b/lib/api/deployments.rb index c5feb49b22f..46b936897f6 100644 --- a/lib/api/deployments.rb +++ b/lib/api/deployments.rb @@ -1,5 +1,5 @@ module API - # Deployments RESTfull API endpoints + # Deployments RESTful API endpoints class Deployments < Grape::API include PaginationParams @@ -8,7 +8,7 @@ module API params do requires :id, type: String, desc: 'The project ID' end - resource :projects do + resource :projects, requirements: { id: %r{[^/]+} } do desc 'Get all deployments of the project' do detail 'This feature was introduced in GitLab 8.11.' success Entities::Deployment diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 400ee7c92aa..5954aea8041 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -49,7 +49,8 @@ module API class ProjectHook < Hook expose :project_id, :issues_events, :merge_requests_events - expose :note_events, :build_events, :pipeline_events, :wiki_page_events + expose :note_events, :pipeline_events, :wiki_page_events + expose :build_events, as: :job_events end class BasicProjectDetails < Grape::Entity @@ -69,9 +70,8 @@ module API class Project < Grape::Entity expose :id, :description, :default_branch, :tag_list - expose :public?, as: :public expose :archived?, as: :archived - expose :visibility_level, :ssh_url_to_repo, :http_url_to_repo, :web_url + expose :visibility, :ssh_url_to_repo, :http_url_to_repo, :web_url expose :owner, using: Entities::UserBasic, unless: ->(project, options) { project.group } expose :name, :name_with_namespace expose :path, :path_with_namespace @@ -81,7 +81,7 @@ module API expose(:issues_enabled) { |project, options| project.feature_available?(:issues, options[:current_user]) } expose(:merge_requests_enabled) { |project, options| project.feature_available?(:merge_requests, options[:current_user]) } expose(:wiki_enabled) { |project, options| project.feature_available?(:wiki, options[:current_user]) } - expose(:builds_enabled) { |project, options| project.feature_available?(:builds, options[:current_user]) } + expose(:jobs_enabled) { |project, options| project.feature_available?(:builds, options[:current_user]) } expose(:snippets_enabled) { |project, options| project.feature_available?(:snippets, options[:current_user]) } expose :created_at, :last_activity_at @@ -94,11 +94,11 @@ module API expose :star_count, :forks_count expose :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[:current_user]) && project.default_issues_tracker? } expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] } - expose :public_builds + expose :public_builds, as: :public_jobs expose :shared_with_groups do |project, options| SharedGroup.represent(project.project_group_links.all, options) end - expose :only_allow_merge_if_build_succeeds + expose :only_allow_merge_if_pipeline_succeeds expose :request_access_enabled expose :only_allow_merge_if_all_discussions_are_resolved @@ -110,7 +110,7 @@ module API expose :storage_size expose :repository_size expose :lfs_objects_size - expose :build_artifacts_size + expose :build_artifacts_size, as: :job_artifacts_size end class Member < UserBasic @@ -132,7 +132,7 @@ module API end class Group < Grape::Entity - expose :id, :name, :path, :description, :visibility_level + expose :id, :name, :path, :description, :visibility expose :lfs_enabled?, as: :lfs_enabled expose :avatar_url expose :web_url @@ -145,7 +145,7 @@ module API expose :storage_size expose :repository_size expose :lfs_objects_size - expose :build_artifacts_size + expose :build_artifacts_size, as: :job_artifacts_size end end end @@ -250,14 +250,11 @@ module API expose :start_date end - class Issue < ProjectEntity + class IssueBasic < ProjectEntity expose :label_names, as: :labels expose :milestone, using: Entities::Milestone expose :assignee, :author, using: Entities::UserBasic - expose :subscribed do |issue, options| - issue.subscribed?(options[:current_user], options[:project] || issue.project) - end expose :user_notes_count expose :upvotes, :downvotes expose :due_date @@ -268,6 +265,12 @@ module API end end + class Issue < IssueBasic + expose :subscribed do |issue, options| + issue.subscribed?(options[:current_user], options[:project] || issue.project) + end + end + class IssuableTimeStats < Grape::Entity expose :time_estimate expose :total_time_spent @@ -280,7 +283,7 @@ module API expose :id end - class MergeRequest < ProjectEntity + class MergeRequestBasic < ProjectEntity expose :target_branch, :source_branch expose :upvotes, :downvotes expose :author, :assignee, using: Entities::UserBasic @@ -288,13 +291,10 @@ module API expose :label_names, as: :labels expose :work_in_progress?, as: :work_in_progress expose :milestone, using: Entities::Milestone - expose :merge_when_build_succeeds + expose :merge_when_pipeline_succeeds expose :merge_status expose :diff_head_sha, as: :sha expose :merge_commit_sha - expose :subscribed do |merge_request, options| - merge_request.subscribed?(options[:current_user], options[:project]) - end expose :user_notes_count expose :should_remove_source_branch?, as: :should_remove_source_branch expose :force_remove_source_branch?, as: :force_remove_source_branch @@ -304,6 +304,12 @@ module API end end + class MergeRequest < MergeRequestBasic + expose :subscribed do |merge_request, options| + merge_request.subscribed?(options[:current_user], options[:project]) + end + end + class MergeRequestChanges < MergeRequest expose :diffs, as: :changes, using: Entities::RepoDiff do |compare, _| compare.raw_diffs(all_diffs: true).to_a @@ -339,9 +345,6 @@ module API expose :created_at, :updated_at expose :system?, as: :system expose :noteable_id, :noteable_type - # upvote? and downvote? are deprecated, always return false - expose(:upvote?) { |note| false } - expose(:downvote?) { |note| false } end class AwardEmoji < Grape::Entity @@ -397,7 +400,8 @@ module API expose :target_type expose :target do |todo, options| - Entities.const_get(todo.target_type).represent(todo.target, options) + target = todo.target_type == 'Commit' ? 'RepoCommit' : todo.target_type + Entities.const_get(target).represent(todo.target, options) end expose :target_url do |todo, options| @@ -451,7 +455,8 @@ module API class ProjectService < Grape::Entity expose :id, :title, :created_at, :updated_at, :active expose :push_events, :issues_events, :merge_requests_events - expose :tag_push_events, :note_events, :build_events, :pipeline_events + expose :tag_push_events, :note_events, :pipeline_events + expose :build_events, as: :job_events # Expose serialized properties expose :properties do |service, options| field_names = service.fields. @@ -554,12 +559,15 @@ module API expose :updated_at expose :home_page_url expose :default_branch_protection - expose :restricted_visibility_levels + expose(:restricted_visibility_levels) do |setting, _options| + setting.restricted_visibility_levels.map { |level| Gitlab::VisibilityLevel.string_level(level) } + end expose :max_attachment_size expose :session_expire_delay - expose :default_project_visibility - expose :default_snippet_visibility - expose :default_group_visibility + expose(:default_project_visibility) { |setting, _options| Gitlab::VisibilityLevel.string_level(setting.default_project_visibility) } + 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 :default_artifacts_expire_in expose :domain_whitelist expose :domain_blacklist_enabled expose :domain_blacklist @@ -592,10 +600,6 @@ module API end end - class TriggerRequest < Grape::Entity - expose :id, :variables - end - class Runner < Grape::Entity expose :id expose :description @@ -620,7 +624,11 @@ module API end end - class BuildArtifactFile < Grape::Entity + class RunnerRegistrationDetails < Grape::Entity + expose :id, :token + end + + class JobArtifactFile < Grape::Entity expose :filename, :size end @@ -628,18 +636,21 @@ module API expose :id, :sha, :ref, :status end - class Build < Grape::Entity + class Job < Grape::Entity expose :id, :status, :stage, :name, :ref, :tag, :coverage expose :created_at, :started_at, :finished_at expose :user, with: User - expose :artifacts_file, using: BuildArtifactFile, if: -> (build, opts) { build.artifacts? } + expose :artifacts_file, using: JobArtifactFile, if: -> (job, opts) { job.artifacts? } expose :commit, with: RepoCommit expose :runner, with: Runner expose :pipeline, with: PipelineBasic end class Trigger < Grape::Entity - expose :token, :created_at, :updated_at, :deleted_at, :last_used + expose :id + expose :token, :description + expose :created_at, :updated_at, :deleted_at, :last_used + expose :owner, using: Entities::UserBasic end class Variable < Grape::Entity @@ -660,14 +671,14 @@ module API end class Environment < EnvironmentBasic - expose :project, using: Entities::Project + expose :project, using: Entities::BasicProjectDetails end class Deployment < Grape::Entity expose :id, :iid, :ref, :sha, :created_at expose :user, using: Entities::UserBasic expose :environment, using: Entities::EnvironmentBasic - expose :deployable, using: Entities::Build + expose :deployable, using: Entities::Job end class RepoLicense < Grape::Entity @@ -694,5 +705,99 @@ module API expose :id, :message, :starts_at, :ends_at, :color, :font expose :active?, as: :active end + + class PersonalAccessToken < Grape::Entity + expose :id, :name, :revoked, :created_at, :scopes + expose :active?, as: :active + expose :expires_at do |personal_access_token| + personal_access_token.expires_at ? personal_access_token.expires_at.strftime("%Y-%m-%d") : nil + end + end + + class PersonalAccessTokenWithToken < PersonalAccessToken + expose :token + end + + class ImpersonationToken < PersonalAccessTokenWithToken + expose :impersonation + end + + module JobRequest + class JobInfo < Grape::Entity + expose :name, :stage + expose :project_id, :project_name + end + + class GitInfo < Grape::Entity + expose :repo_url, :ref, :sha, :before_sha + expose :ref_type do |model| + if model.tag + 'tag' + else + 'branch' + end + end + end + + class RunnerInfo < Grape::Entity + expose :timeout + end + + class Step < Grape::Entity + expose :name, :script, :timeout, :when, :allow_failure + end + + class Image < Grape::Entity + expose :name + end + + class Artifacts < Grape::Entity + expose :name, :untracked, :paths, :when, :expire_in + end + + class Cache < Grape::Entity + expose :key, :untracked, :paths + end + + class Credentials < Grape::Entity + expose :type, :url, :username, :password + end + + class ArtifactFile < Grape::Entity + expose :filename, :size + end + + class Dependency < Grape::Entity + expose :id, :name, :token + expose :artifacts_file, using: ArtifactFile, if: ->(job, _) { job.artifacts? } + end + + class Response < Grape::Entity + expose :id + expose :token + expose :allow_git_fetch + + expose :job_info, using: JobInfo do |model| + model + end + + expose :git_info, using: GitInfo do |model| + model + end + + expose :runner_info, using: RunnerInfo do |model| + model + end + + expose :variables + expose :steps, using: Step + expose :image, using: Image + expose :services, using: Image + expose :artifacts, using: Artifacts + expose :cache, using: Cache + expose :credentials, using: Credentials + expose :dependencies, using: Dependency + end + end end end diff --git a/lib/api/environments.rb b/lib/api/environments.rb index 1a7e68f0528..945771d46f3 100644 --- a/lib/api/environments.rb +++ b/lib/api/environments.rb @@ -9,7 +9,7 @@ module API params do requires :id, type: String, desc: 'The project ID' end - resource :projects do + resource :projects, requirements: { id: %r{[^/]+} } do desc 'Get all environments of the project' do detail 'This feature was introduced in GitLab 8.11.' success Entities::Environment @@ -79,7 +79,24 @@ module API environment = user_project.environments.find(params[:environment_id]) - present environment.destroy, with: Entities::Environment + environment.destroy + end + + desc 'Stops an existing environment' do + success Entities::Environment + end + params do + requires :environment_id, type: Integer, desc: 'The environment ID' + end + post ':id/environments/:environment_id/stop' do + authorize! :create_deployment, user_project + + environment = user_project.environments.find(params[:environment_id]) + + environment.stop_with_action!(current_user) + + status 200 + present environment, with: Entities::Environment end end end diff --git a/lib/api/files.rb b/lib/api/files.rb index 500f9d3c787..33fc970dc09 100644 --- a/lib/api/files.rb +++ b/lib/api/files.rb @@ -14,6 +14,19 @@ module API } end + def assign_file_vars! + authorize! :download_code, user_project + + @commit = user_project.commit(params[:ref]) + not_found!('Commit') unless @commit + + @repo = user_project.repository + @blob = @repo.blob_at(@commit.sha, params[:file_path]) + + not_found!('File') unless @blob + @blob.load_all_data!(@repo) + end + def commit_response(attrs) { file_path: attrs[:file_path], @@ -22,7 +35,7 @@ module API end params :simple_file_params do - requires :file_path, type: String, desc: 'The path to new file. Ex. lib/class.rb' + requires :file_path, type: String, desc: 'The url encoded path to the file. Ex. lib%2Fclass%2Erb' requires :branch, type: String, desc: 'The name of branch' requires :commit_message, type: String, desc: 'Commit Message' optional :author_email, type: String, desc: 'The email of the author' @@ -39,35 +52,36 @@ module API params do requires :id, type: String, desc: 'The project ID' end - resource :projects do - desc 'Get a file from repository' + resource :projects, requirements: { id: %r{[^/]+} } do + desc 'Get raw file contents from the repository' params do - requires :file_path, type: String, desc: 'The path to the file. Ex. lib/class.rb' - requires :ref, type: String, desc: 'The name of branch, tag, or commit' + requires :file_path, type: String, desc: 'The url encoded path to the file. Ex. lib%2Fclass%2Erb' + requires :ref, type: String, desc: 'The name of branch, tag commit' end - get ":id/repository/files" do - authorize! :download_code, user_project - - commit = user_project.commit(params[:ref]) - not_found!('Commit') unless commit + get ":id/repository/files/:file_path/raw" do + assign_file_vars! - repo = user_project.repository - blob = repo.blob_at(commit.sha, params[:file_path]) - not_found!('File') unless blob + send_git_blob @repo, @blob + end - blob.load_all_data!(repo) - status(200) + desc 'Get a file from the repository' + params do + requires :file_path, type: String, desc: 'The url encoded path to the file. Ex. lib%2Fclass%2Erb' + requires :ref, type: String, desc: 'The name of branch, tag or commit' + end + get ":id/repository/files/:file_path", requirements: { file_path: /.+/ } do + assign_file_vars! { - file_name: blob.name, - file_path: blob.path, - size: blob.size, + file_name: @blob.name, + file_path: @blob.path, + size: @blob.size, encoding: "base64", - content: Base64.strict_encode64(blob.data), + content: Base64.strict_encode64(@blob.data), ref: params[:ref], - blob_id: blob.id, - commit_id: commit.id, - last_commit_id: repo.last_commit_id_for_path(commit.sha, params[:file_path]) + blob_id: @blob.id, + commit_id: @commit.id, + last_commit_id: @repo.last_commit_id_for_path(@commit.sha, params[:file_path]) } end @@ -75,7 +89,7 @@ module API params do use :extended_file_params end - post ":id/repository/files" do + post ":id/repository/files/:file_path", requirements: { file_path: /.+/ } do authorize! :push_code, user_project file_params = declared_params(include_missing: false) @@ -93,7 +107,7 @@ module API params do use :extended_file_params end - put ":id/repository/files" do + put ":id/repository/files/:file_path", requirements: { file_path: /.+/ } do authorize! :push_code, user_project file_params = declared_params(include_missing: false) @@ -112,16 +126,13 @@ module API params do use :simple_file_params end - delete ":id/repository/files" do + delete ":id/repository/files/:file_path", requirements: { file_path: /.+/ } do authorize! :push_code, user_project file_params = declared_params(include_missing: false) result = ::Files::DestroyService.new(user_project, current_user, commit_params(file_params)).execute - if result[:status] == :success - status(200) - commit_response(file_params) - else + if result[:status] != :success render_api_error!(result[:message], 400) end end diff --git a/lib/api/groups.rb b/lib/api/groups.rb index 9f29c4466ab..8f3799417e3 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -7,7 +7,7 @@ module API helpers do params :optional_params do optional :description, type: String, desc: 'The description of the group' - optional :visibility_level, type: Integer, desc: 'The visibility level of the group' + optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The visibility of the group' optional :lfs_enabled, type: Boolean, desc: 'Enable/disable LFS for the projects in this group' optional :request_access_enabled, type: Boolean, desc: 'Allow users to request member access' end @@ -36,12 +36,15 @@ module API optional :skip_groups, type: Array[Integer], desc: 'Array of group ids to exclude from list' optional :all_available, type: Boolean, desc: 'Show all group that you have access to' optional :search, type: String, desc: 'Search for a specific group' + optional :owned, type: Boolean, default: false, desc: 'Limit by owned by authenticated user' optional :order_by, type: String, values: %w[name path], default: 'name', desc: 'Order by name or path' optional :sort, type: String, values: %w[asc desc], default: 'asc', desc: 'Sort by asc (ascending) or desc (descending)' use :pagination end get do - groups = if current_user.admin + groups = if params[:owned] + current_user.owned_groups + elsif current_user.admin Group.all elsif params[:all_available] GroupsFinder.new.execute(current_user) @@ -56,17 +59,6 @@ module API present_groups groups, statistics: params[:statistics] && current_user.is_admin? end - desc 'Get list of owned groups for authenticated user' do - success Entities::Group - end - params do - use :pagination - use :statistics_params - end - get '/owned' do - present_groups current_user.owned_groups, statistics: params[:statistics] - end - desc 'Create a group. Available only for users who can create groups.' do success Entities::Group end @@ -92,7 +84,7 @@ module API params do requires :id, type: String, desc: 'The ID of a group' end - resource :groups do + resource :groups, requirements: { id: %r{[^/]+} } do desc 'Update a group. Available only for users who can administrate groups.' do success Entities::Group end @@ -100,7 +92,7 @@ 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 - at_least_one_of :name, :path, :description, :visibility_level, + at_least_one_of :name, :path, :description, :visibility, :lfs_enabled, :request_access_enabled end put ':id' do @@ -134,7 +126,7 @@ module API end params do optional :archived, type: Boolean, default: false, desc: 'Limit by archived status' - optional :visibility, type: String, values: %w[public internal private], + optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'Limit by visibility' optional :search, type: String, desc: 'Return list of authorized projects matching the search criteria' optional :order_by, type: String, values: %w[id name path created_at updated_at last_activity_at], @@ -162,7 +154,7 @@ module API params do requires :project_id, type: String, desc: 'The ID or path of the project' end - post ":id/projects/:project_id" do + post ":id/projects/:project_id", requirements: { project_id: /.+/ } do authenticated_as_admin! group = find_group!(params[:id]) project = find_project!(params[:project_id]) diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 0fd2b1587e3..3c173b544aa 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -3,7 +3,7 @@ module API include Gitlab::Utils include Helpers::Pagination - SUDO_HEADER = "HTTP_SUDO" + SUDO_HEADER = "HTTP_SUDO".freeze SUDO_PARAM = :sudo def declared_params(options = {}) @@ -82,22 +82,22 @@ module API label || not_found!('Label') end - def find_project_issue(id) - IssuesFinder.new(current_user, project_id: user_project.id).find(id) + def find_project_issue(iid) + IssuesFinder.new(current_user, project_id: user_project.id).find_by!(iid: iid) end - def find_project_merge_request(id) - MergeRequestsFinder.new(current_user, project_id: user_project.id).find(id) + def find_project_merge_request(iid) + MergeRequestsFinder.new(current_user, project_id: user_project.id).find_by!(iid: iid) end - def find_merge_request_with_access(id, access_level = :read_merge_request) - merge_request = user_project.merge_requests.find(id) + def find_merge_request_with_access(iid, access_level = :read_merge_request) + merge_request = user_project.merge_requests.find_by!(iid: iid) authorize! access_level, merge_request merge_request end def authenticate! - unauthorized! unless current_user + unauthorized! unless current_user && can?(current_user, :access_api) end def authenticate_non_get! @@ -126,7 +126,7 @@ module API forbidden! unless current_user.is_admin? end - def authorize!(action, subject = nil) + def authorize!(action, subject = :global) forbidden! unless can?(current_user, action, subject) end @@ -144,7 +144,7 @@ module API end end - def can?(object, action, subject) + def can?(object, action, subject = :global) Ability.allowed?(object, action, subject) end @@ -174,6 +174,10 @@ module API items.where(iid: iid) end + def filter_by_search(items, text) + items.search(text) + end + # error helpers def forbidden!(reason = nil) @@ -219,6 +223,10 @@ module API render_api_error!('204 No Content', 204) end + def accepted! + render_api_error!('202 Accepted', 202) + end + def render_validation_error!(model) if model.errors.any? render_api_error!(model.errors.messages || '400 Bad Request', 400) @@ -254,6 +262,10 @@ module API # project helpers def filter_projects(projects) + if params[:membership] + projects = projects.merge(current_user.authorized_projects) + end + if params[:owned] projects = projects.merge(current_user.owned_projects) end @@ -334,16 +346,17 @@ module API def initial_current_user return @initial_current_user if defined?(@initial_current_user) + Gitlab::Auth::UniqueIpsLimiter.limit_user! do + @initial_current_user ||= find_user_by_private_token(scopes: @scopes) + @initial_current_user ||= doorkeeper_guard(scopes: @scopes) + @initial_current_user ||= find_user_from_warden - @initial_current_user ||= find_user_by_private_token(scopes: @scopes) - @initial_current_user ||= doorkeeper_guard(scopes: @scopes) - @initial_current_user ||= find_user_from_warden + unless @initial_current_user && Gitlab::UserAccess.new(@initial_current_user).allowed? + @initial_current_user = nil + end - unless @initial_current_user && Gitlab::UserAccess.new(@initial_current_user).allowed? - @initial_current_user = nil + @initial_current_user end - - @initial_current_user end def sudo! @@ -386,14 +399,6 @@ module API header(*Gitlab::Workhorse.send_git_archive(repository, ref: ref, format: format)) end - def issue_entity(project) - if project.has_external_issue_tracker? - Entities::ExternalIssue - else - Entities::Issue - end - end - # The Grape Error Middleware only has access to env but no params. We workaround this by # defining a method that returns the right value. def define_params_for_grape_middleware diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb index 080a6274957..2135a787b11 100644 --- a/lib/api/helpers/internal_helpers.rb +++ b/lib/api/helpers/internal_helpers.rb @@ -9,11 +9,11 @@ module API # In addition, they may have a '.git' extension and multiple namespaces # # Transform all these cases to 'namespace/project' - def clean_project_path(project_path, storage_paths = Repository.storages.values) + def clean_project_path(project_path, storages = Gitlab.config.repositories.storages.values) project_path = project_path.sub(/\.git\z/, '') - storage_paths.each do |storage_path| - storage_path = File.expand_path(storage_path) + storages.each do |storage| + storage_path = File.expand_path(storage['path']) if project_path.start_with?(storage_path) project_path = project_path.sub(storage_path, '') diff --git a/lib/api/helpers/runner.rb b/lib/api/helpers/runner.rb new file mode 100644 index 00000000000..74848a6e144 --- /dev/null +++ b/lib/api/helpers/runner.rb @@ -0,0 +1,69 @@ +module API + module Helpers + module Runner + JOB_TOKEN_HEADER = 'HTTP_JOB_TOKEN'.freeze + JOB_TOKEN_PARAM = :token + UPDATE_RUNNER_EVERY = 10 * 60 + + def runner_registration_token_valid? + ActiveSupport::SecurityUtils.variable_size_secure_compare(params[:token], + current_application_settings.runners_registration_token) + end + + def get_runner_version_from_params + return unless params['info'].present? + attributes_for_keys(%w(name version revision platform architecture), params['info']) + end + + def authenticate_runner! + forbidden! unless current_runner + end + + def current_runner + @runner ||= ::Ci::Runner.find_by_token(params[:token].to_s) + end + + def update_runner_info + return unless update_runner? + + current_runner.contacted_at = Time.now + current_runner.assign_attributes(get_runner_version_from_params) + current_runner.save if current_runner.changed? + end + + def update_runner? + # Use a random threshold to prevent beating DB updates. + # It generates a distribution between [40m, 80m]. + # + contacted_at_max_age = UPDATE_RUNNER_EVERY + Random.rand(UPDATE_RUNNER_EVERY) + + current_runner.contacted_at.nil? || + (Time.now - current_runner.contacted_at) >= contacted_at_max_age + end + + def validate_job!(job) + not_found! unless job + + yield if block_given? + + forbidden!('Project has been deleted!') unless job.project + forbidden!('Job has been erased!') if job.erased? + end + + def authenticate_job!(job) + validate_job!(job) do + forbidden! unless job_token_valid?(job) + end + end + + def job_token_valid?(job) + token = (params[JOB_TOKEN_PARAM] || env[JOB_TOKEN_HEADER]).to_s + token && job.valid_token?(token) + end + + def max_artifacts_size + current_application_settings.max_artifacts_size.megabytes.to_i + end + end + end +end diff --git a/lib/api/internal.rb b/lib/api/internal.rb index d235977fbd8..7eed93aba00 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -132,6 +132,18 @@ module API { success: true, recovery_codes: codes } end + + post "/notify_post_receive" do + status 200 + + return unless Gitlab::GitalyClient.enabled? + + begin + Gitlab::GitalyClient::Notifications.new.post_receive(params[:repo_path]) + rescue GRPC::Unavailable => e + render_api_error(e, 500) + end + end end end end diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 6d30c5d81b1..fd2674910d2 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -25,6 +25,7 @@ module API optional :sort, type: String, values: %w[asc desc], default: 'desc', desc: 'Return issues sorted in `asc` or `desc` order.' optional :milestone, type: String, desc: 'Return issues for a specific milestone' + optional :iids, type: Array[Integer], desc: 'The IID array of issues' use :pagination end @@ -40,7 +41,7 @@ module API resource :issues do desc "Get currently authenticated user's issues" do - success Entities::Issue + success Entities::IssueBasic end params do optional :state, type: String, values: %w[opened closed all], default: 'all', @@ -50,16 +51,16 @@ module API get do issues = find_issues(scope: 'authored') - present paginate(issues), with: Entities::Issue, current_user: current_user + present paginate(issues), with: Entities::IssueBasic, current_user: current_user end end params do requires :id, type: String, desc: 'The ID of a group' end - resource :groups do + resource :groups, requirements: { id: %r{[^/]+} } do desc 'Get a list of group issues' do - success Entities::Issue + success Entities::IssueBasic end params do optional :state, type: String, values: %w[opened closed all], default: 'opened', @@ -71,18 +72,18 @@ module API issues = find_issues(group_id: group.id, state: params[:state] || 'opened') - present paginate(issues), with: Entities::Issue, current_user: current_user + present paginate(issues), with: Entities::IssueBasic, current_user: current_user end end params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects do + resource :projects, requirements: { id: %r{[^/]+} } do include TimeTrackingEndpoints desc 'Get a list of project issues' do - success Entities::Issue + success Entities::IssueBasic end params do optional :state, type: String, values: %w[opened closed all], default: 'all', @@ -90,21 +91,21 @@ module API use :issues_params end get ":id/issues" do - project = find_project(params[:id]) + project = find_project!(params[:id]) issues = find_issues(project_id: project.id) - present paginate(issues), with: Entities::Issue, current_user: current_user, project: user_project + present paginate(issues), with: Entities::IssueBasic, current_user: current_user, project: user_project end desc 'Get a single project issue' do success Entities::Issue end params do - requires :issue_id, type: Integer, desc: 'The ID of a project issue' + requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue' end - get ":id/issues/:issue_id" do - issue = find_project_issue(params[:issue_id]) + get ":id/issues/:issue_iid" do + issue = find_project_issue(params[:issue_iid]) present issue, with: Entities::Issue, current_user: current_user, project: user_project end @@ -115,8 +116,10 @@ module API requires :title, type: String, desc: 'The title of an issue' optional :created_at, type: DateTime, desc: 'Date time when the issue was created. Available only for admins and project owners.' - optional :merge_request_for_resolving_discussions, type: Integer, + optional :merge_request_to_resolve_discussions_of, type: Integer, desc: 'The IID of a merge request for which to resolve discussions' + optional :discussion_to_resolve, type: String, + desc: 'The ID of a discussion to resolve, also pass `merge_request_to_resolve_discussions_of`' use :issue_params end post ':id/issues' do @@ -127,12 +130,6 @@ module API issue_params = declared_params(include_missing: false) - if merge_request_iid = params[:merge_request_for_resolving_discussions] - issue_params[:merge_request_for_resolving_discussions] = MergeRequestsFinder.new(current_user, project_id: user_project.id). - execute. - find_by(iid: merge_request_iid) - end - issue = ::Issues::CreateService.new(user_project, current_user, issue_params.merge(request: request, api: true)).execute @@ -151,7 +148,7 @@ module API success Entities::Issue end params do - requires :issue_id, type: Integer, desc: 'The ID of a project issue' + requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue' optional :title, type: String, desc: 'The title of an issue' optional :updated_at, type: DateTime, desc: 'Date time when the issue was updated. Available only for admins and project owners.' @@ -160,8 +157,8 @@ module API at_least_one_of :title, :description, :assignee_id, :milestone_id, :labels, :created_at, :due_date, :confidential, :state_event end - put ':id/issues/:issue_id' do - issue = user_project.issues.find(params.delete(:issue_id)) + put ':id/issues/:issue_iid' do + 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 owners @@ -188,11 +185,11 @@ module API success Entities::Issue end params do - requires :issue_id, type: Integer, desc: 'The ID of a project issue' + requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue' requires :to_project_id, type: Integer, desc: 'The ID of the new project' end - post ':id/issues/:issue_id/move' do - issue = user_project.issues.find_by(id: params[:issue_id]) + post ':id/issues/:issue_iid/move' do + issue = user_project.issues.find_by(iid: params[:issue_iid]) not_found!('Issue') unless issue new_project = Project.find_by(id: params[:to_project_id]) @@ -208,10 +205,10 @@ module API desc 'Delete a project issue' params do - requires :issue_id, type: Integer, desc: 'The ID of a project issue' + requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue' end - delete ":id/issues/:issue_id" do - issue = user_project.issues.find_by(id: params[:issue_id]) + delete ":id/issues/:issue_iid" do + issue = user_project.issues.find_by(iid: params[:issue_iid]) not_found!('Issue') unless issue authorize!(:destroy_issue, issue) diff --git a/lib/api/builds.rb b/lib/api/jobs.rb index 44fe0fc4a95..ffab0aafe59 100644 --- a/lib/api/builds.rb +++ b/lib/api/jobs.rb @@ -1,5 +1,5 @@ module API - class Builds < Grape::API + class Jobs < Grape::API include PaginationParams before { authenticate! } @@ -7,16 +7,19 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects do + resource :projects, requirements: { id: %r{[^/]+} } do helpers do params :optional_scope do optional :scope, types: [String, Array[String]], desc: 'The scope of builds to show', - values: ['pending', 'running', 'failed', 'success', 'canceled'], + values: ::CommitStatus::AVAILABLE_STATUSES, coerce_with: ->(scope) { - if scope.is_a?(String) + case scope + when String [scope] - elsif scope.is_a?(Hashie::Mash) + when Hashie::Mash scope.values + when Hashie::Array + scope else ['unknown'] end @@ -24,79 +27,72 @@ module API end end - desc 'Get a project builds' do - success Entities::Build + desc 'Get a projects jobs' do + success Entities::Job end params do use :optional_scope use :pagination end - get ':id/builds' do + get ':id/jobs' do builds = user_project.builds.order('id DESC') builds = filter_builds(builds, params[:scope]) - present paginate(builds), with: Entities::Build, - user_can_download_artifacts: can?(current_user, :read_build, user_project) + present paginate(builds), with: Entities::Job end - desc 'Get builds for a specific commit of a project' do - success Entities::Build + desc 'Get pipeline jobs' do + success Entities::Job end params do - requires :sha, type: String, desc: 'The SHA id of a commit' + requires :pipeline_id, type: Integer, desc: 'The pipeline ID' use :optional_scope use :pagination end - get ':id/repository/commits/:sha/builds' do - authorize_read_builds! - - return not_found! unless user_project.commit(params[:sha]) - - pipelines = user_project.pipelines.where(sha: params[:sha]) - builds = user_project.builds.where(pipeline: pipelines).order('id DESC') + get ':id/pipelines/:pipeline_id/jobs' do + pipeline = user_project.pipelines.find(params[:pipeline_id]) + builds = pipeline.builds builds = filter_builds(builds, params[:scope]) - present paginate(builds), with: Entities::Build, - user_can_download_artifacts: can?(current_user, :read_build, user_project) + present paginate(builds), with: Entities::Job end - desc 'Get a specific build of a project' do - success Entities::Build + desc 'Get a specific job of a project' do + success Entities::Job end params do - requires :build_id, type: Integer, desc: 'The ID of a build' + requires :job_id, type: Integer, desc: 'The ID of a job' end - get ':id/builds/:build_id' do + get ':id/jobs/:job_id' do authorize_read_builds! - build = get_build!(params[:build_id]) + build = get_build!(params[:job_id]) - present build, with: Entities::Build, - user_can_download_artifacts: can?(current_user, :read_build, user_project) + present build, with: Entities::Job end - desc 'Download the artifacts file from build' do + desc 'Download the artifacts file from a job' do detail 'This feature was introduced in GitLab 8.5' end params do - requires :build_id, type: Integer, desc: 'The ID of a build' + requires :job_id, type: Integer, desc: 'The ID of a job' end - get ':id/builds/:build_id/artifacts' do + get ':id/jobs/:job_id/artifacts' do authorize_read_builds! - build = get_build!(params[:build_id]) + build = get_build!(params[:job_id]) present_artifacts!(build.artifacts_file) end - desc 'Download the artifacts file from build' do + desc 'Download the artifacts file from a job' do detail 'This feature was introduced in GitLab 8.10' end params do requires :ref_name, type: String, desc: 'The ref from repository' - requires :job, type: String, desc: 'The name for the build' + requires :job, type: String, desc: 'The name for the job' end - get ':id/builds/artifacts/:ref_name/download', + get ':id/jobs/artifacts/:ref_name/download', requirements: { ref_name: /.+/ } do authorize_read_builds! @@ -109,14 +105,14 @@ module API # TODO: We should use `present_file!` and leave this implementation for backward compatibility (when build trace # is saved in the DB instead of file). But before that, we need to consider how to replace the value of # `runners_token` with some mask (like `xxxxxx`) when sending trace file directly by workhorse. - desc 'Get a trace of a specific build of a project' + desc 'Get a trace of a specific job of a project' params do - requires :build_id, type: Integer, desc: 'The ID of a build' + requires :job_id, type: Integer, desc: 'The ID of a job' end - get ':id/builds/:build_id/trace' do + get ':id/jobs/:job_id/trace' do authorize_read_builds! - build = get_build!(params[:build_id]) + build = get_build!(params[:job_id]) header 'Content-Disposition', "infile; filename=\"#{build.id}.log\"" content_type 'text/plain' @@ -126,96 +122,91 @@ module API body trace end - desc 'Cancel a specific build of a project' do - success Entities::Build + desc 'Cancel a specific job of a project' do + success Entities::Job end params do - requires :build_id, type: Integer, desc: 'The ID of a build' + requires :job_id, type: Integer, desc: 'The ID of a job' end - post ':id/builds/:build_id/cancel' do + post ':id/jobs/:job_id/cancel' do authorize_update_builds! - build = get_build!(params[:build_id]) + build = get_build!(params[:job_id]) build.cancel - present build, with: Entities::Build, - user_can_download_artifacts: can?(current_user, :read_build, user_project) + present build, with: Entities::Job end desc 'Retry a specific build of a project' do - success Entities::Build + success Entities::Job end params do - requires :build_id, type: Integer, desc: 'The ID of a build' + requires :job_id, type: Integer, desc: 'The ID of a build' end - post ':id/builds/:build_id/retry' do + post ':id/jobs/:job_id/retry' do authorize_update_builds! - build = get_build!(params[:build_id]) - return forbidden!('Build is not retryable') unless build.retryable? + build = get_build!(params[:job_id]) + return forbidden!('Job is not retryable') unless build.retryable? build = Ci::Build.retry(build, current_user) - present build, with: Entities::Build, - user_can_download_artifacts: can?(current_user, :read_build, user_project) + present build, with: Entities::Job end - desc 'Erase build (remove artifacts and build trace)' do - success Entities::Build + desc 'Erase job (remove artifacts and the trace)' do + success Entities::Job end params do - requires :build_id, type: Integer, desc: 'The ID of a build' + requires :job_id, type: Integer, desc: 'The ID of a build' end - post ':id/builds/:build_id/erase' do + post ':id/jobs/:job_id/erase' do authorize_update_builds! - build = get_build!(params[:build_id]) - return forbidden!('Build is not erasable!') unless build.erasable? + build = get_build!(params[:job_id]) + return forbidden!('Job is not erasable!') unless build.erasable? build.erase(erased_by: current_user) - present build, with: Entities::Build, - user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project) + present build, with: Entities::Job end desc 'Keep the artifacts to prevent them from being deleted' do - success Entities::Build + success Entities::Job end params do - requires :build_id, type: Integer, desc: 'The ID of a build' + requires :job_id, type: Integer, desc: 'The ID of a job' end - post ':id/builds/:build_id/artifacts/keep' do + post ':id/jobs/:job_id/artifacts/keep' do authorize_update_builds! - build = get_build!(params[:build_id]) + build = get_build!(params[:job_id]) return not_found!(build) unless build.artifacts? build.keep_artifacts! status 200 - present build, with: Entities::Build, - user_can_download_artifacts: can?(current_user, :read_build, user_project) + present build, with: Entities::Job end - desc 'Trigger a manual build' do - success Entities::Build + desc 'Trigger a manual job' do + success Entities::Job detail 'This feature was added in GitLab 8.11' end params do - requires :build_id, type: Integer, desc: 'The ID of a Build' + requires :job_id, type: Integer, desc: 'The ID of a Job' end - post ":id/builds/:build_id/play" do + post ":id/jobs/:job_id/play" do authorize_read_builds! - build = get_build!(params[:build_id]) + build = get_build!(params[:job_id]) bad_request!("Unplayable Job") unless build.playable? build.play(current_user) status 200 - present build, with: Entities::Build, - user_can_download_artifacts: can?(current_user, :read_build, user_project) + present build, with: Entities::Job end end diff --git a/lib/api/labels.rb b/lib/api/labels.rb index d2955af3f95..d9a3cb7bb6b 100644 --- a/lib/api/labels.rb +++ b/lib/api/labels.rb @@ -1,13 +1,13 @@ module API class Labels < Grape::API include PaginationParams - + before { authenticate! } params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects do + resource :projects, requirements: { id: %r{[^/]+} } do desc 'Get all labels of the project' do success Entities::Label end @@ -56,7 +56,7 @@ module API label = user_project.labels.find_by(title: params[:name]) not_found!('Label') unless label - present label.destroy, with: Entities::Label, current_user: current_user, project: user_project + label.destroy end desc 'Update an existing label. At least one optional parameter is required.' do diff --git a/lib/api/members.rb b/lib/api/members.rb index d1d78775c6d..c200e46a328 100644 --- a/lib/api/members.rb +++ b/lib/api/members.rb @@ -10,7 +10,7 @@ module API params do requires :id, type: String, desc: "The #{source_type} ID" end - resource source_type.pluralize do + resource source_type.pluralize, requirements: { id: %r{[^/]+} } do desc 'Gets a list of group or project members viewable by the authenticated user.' do success Entities::Member end @@ -55,7 +55,6 @@ module API authorize_admin_source!(source_type, source) member = source.members.find_by(user_id: params[:user_id]) - conflict!('Member already exists') if member member = source.add_user(params[:user_id], params[:access_level], current_user: current_user, expires_at: params[:expires_at]) @@ -63,9 +62,6 @@ module API if member.persisted? && member.valid? present member.user, with: Entities::Member, member: member else - # This is to ensure back-compatibility but 400 behavior should be used - # for all validation errors in 9.0! - render_api_error!('Access level is not known', 422) if member.errors.key?(:access_level) render_validation_error!(member) end end @@ -79,18 +75,14 @@ module API optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY' end put ":id/members/:user_id" do - source = find_source(source_type, params[:id]) + source = find_source(source_type, params.delete(:id)) authorize_admin_source!(source_type, source) - member = source.members.find_by!(user_id: params[:user_id]) - attrs = attributes_for_keys [:access_level, :expires_at] + member = source.members.find_by!(user_id: params.delete(:user_id)) - if member.update_attributes(attrs) + if member.update_attributes(declared_params(include_missing: false)) present member.user, with: Entities::Member, member: member else - # This is to ensure back-compatibility but 400 behavior should be used - # for all validation errors in 9.0! - render_api_error!('Access level is not known', 422) if member.errors.key?(:access_level) render_validation_error!(member) end end @@ -101,24 +93,10 @@ module API end delete ":id/members/:user_id" do source = find_source(source_type, params[:id]) + # Ensure that memeber exists + source.members.find_by!(user_id: params[:user_id]) - # This is to ensure back-compatibility but find_by! should be used - # in that casse in 9.0! - member = source.members.find_by(user_id: params[:user_id]) - - # This is to ensure back-compatibility but this should be removed in - # favor of find_by! in 9.0! - not_found!("Member: user_id:#{params[:user_id]}") if source_type == 'group' && member.nil? - - # This is to ensure back-compatibility but 204 behavior should be used - # for all DELETE endpoints in 9.0! - if member.nil? - { message: "Access revoked", id: params[:user_id].to_i } - else - ::Members::DestroyService.new(source, current_user, declared_params).execute - - present member.user, with: Entities::Member, member: member - end + ::Members::DestroyService.new(source, current_user, declared_params).execute end end end diff --git a/lib/api/merge_request_diffs.rb b/lib/api/merge_request_diffs.rb index 4901a7cfea6..4b79eac2b8b 100644 --- a/lib/api/merge_request_diffs.rb +++ b/lib/api/merge_request_diffs.rb @@ -5,19 +5,21 @@ module API before { authenticate! } - resource :projects do + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects, requirements: { id: %r{[^/]+} } do desc 'Get a list of merge request diff versions' do detail 'This feature was introduced in GitLab 8.12.' success Entities::MergeRequestDiff end params do - requires :id, type: String, desc: 'The ID of a project' - requires :merge_request_id, type: Integer, desc: 'The ID of a merge request' + requires :merge_request_iid, type: Integer, desc: 'The IID of a merge request' use :pagination end - get ":id/merge_requests/:merge_request_id/versions" do - merge_request = find_merge_request_with_access(params[:merge_request_id]) + get ":id/merge_requests/:merge_request_iid/versions" do + merge_request = find_merge_request_with_access(params[:merge_request_iid]) present paginate(merge_request.merge_request_diffs), with: Entities::MergeRequestDiff end @@ -28,13 +30,12 @@ module API end params do - requires :id, type: String, desc: 'The ID of a project' - requires :merge_request_id, type: Integer, desc: 'The ID of a merge request' + requires :merge_request_iid, type: Integer, desc: 'The IID of a merge request' requires :version_id, type: Integer, desc: 'The ID of a merge request diff version' end - get ":id/merge_requests/:merge_request_id/versions/:version_id" do - merge_request = find_merge_request_with_access(params[:merge_request_id]) + get ":id/merge_requests/:merge_request_iid/versions/:version_id" do + merge_request = find_merge_request_with_access(params[:merge_request_iid]) present merge_request.merge_request_diffs.find(params[:version_id]), with: Entities::MergeRequestDiffFull end diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index bdd764abfeb..5cc807d5bff 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -7,7 +7,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects do + resource :projects, requirements: { id: %r{[^/]+} } do include TimeTrackingEndpoints helpers do @@ -25,6 +25,14 @@ module API render_api_error!(errors, 400) end + def issue_entity(project) + if project.has_external_issue_tracker? + Entities::ExternalIssue + else + Entities::IssueBasic + end + end + 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' @@ -35,7 +43,7 @@ module API end desc 'List merge requests' do - success Entities::MergeRequest + success Entities::MergeRequestBasic end params do optional :state, type: String, values: %w[opened closed merged all], default: 'all', @@ -62,7 +70,7 @@ module API end merge_requests = merge_requests.reorder(params[:order_by] => params[:sort]) - present paginate(merge_requests), with: Entities::MergeRequest, current_user: current_user, project: user_project + present paginate(merge_requests), with: Entities::MergeRequestBasic, current_user: current_user, project: user_project end desc 'Create a merge request' do @@ -93,23 +101,23 @@ module API desc 'Delete a merge request' params do - requires :merge_request_id, type: Integer, desc: 'The ID of a merge request' + requires :merge_request_iid, type: Integer, desc: 'The IID of a merge request' end - delete ":id/merge_requests/:merge_request_id" do - merge_request = find_project_merge_request(params[:merge_request_id]) + delete ":id/merge_requests/:merge_request_iid" do + merge_request = find_project_merge_request(params[:merge_request_iid]) authorize!(:destroy_merge_request, merge_request) merge_request.destroy end params do - requires :merge_request_id, type: Integer, desc: 'The ID of a merge request' + requires :merge_request_iid, type: Integer, desc: 'The IID of a merge request' end desc 'Get a single merge request' do success Entities::MergeRequest end - get ':id/merge_requests/:merge_request_id' do - merge_request = find_merge_request_with_access(params[:merge_request_id]) + get ':id/merge_requests/:merge_request_iid' do + merge_request = find_merge_request_with_access(params[:merge_request_iid]) present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project end @@ -117,8 +125,8 @@ module API desc 'Get the commits of a merge request' do success Entities::RepoCommit end - get ':id/merge_requests/:merge_request_id/commits' do - merge_request = find_merge_request_with_access(params[:merge_request_id]) + get ':id/merge_requests/:merge_request_iid/commits' do + merge_request = find_merge_request_with_access(params[:merge_request_iid]) commits = ::Kaminari.paginate_array(merge_request.commits) present paginate(commits), with: Entities::RepoCommit @@ -127,8 +135,8 @@ module API desc 'Show the merge request changes' do success Entities::MergeRequestChanges end - get ':id/merge_requests/:merge_request_id/changes' do - merge_request = find_merge_request_with_access(params[:merge_request_id]) + get ':id/merge_requests/:merge_request_iid/changes' do + merge_request = find_merge_request_with_access(params[:merge_request_iid]) present merge_request, with: Entities::MergeRequestChanges, current_user: current_user end @@ -146,8 +154,8 @@ module API :milestone_id, :labels, :state_event, :remove_source_branch end - put ':id/merge_requests/:merge_request_id' do - merge_request = find_merge_request_with_access(params.delete(:merge_request_id), :update_merge_request) + put ':id/merge_requests/:merge_request_iid' do + 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? @@ -168,12 +176,12 @@ module API optional :merge_commit_message, type: String, desc: 'Custom merge commit message' optional :should_remove_source_branch, type: Boolean, desc: 'When true, the source branch will be deleted if possible' - optional :merge_when_build_succeeds, type: Boolean, - desc: 'When true, this merge request will be merged when the pipeline succeeds' + optional :merge_when_pipeline_succeeds, type: Boolean, + desc: 'When true, this merge request will be merged when the pipeline succeeds' optional :sha, type: String, desc: 'When present, must have the HEAD SHA of the source branch' end - put ':id/merge_requests/:merge_request_id/merge' do - merge_request = find_project_merge_request(params[:merge_request_id]) + put ':id/merge_requests/:merge_request_iid/merge' do + merge_request = find_project_merge_request(params[:merge_request_iid]) # Merge request can not be merged # because user dont have permissions to push into target branch @@ -192,7 +200,7 @@ module API should_remove_source_branch: params[:should_remove_source_branch] } - if params[:merge_when_build_succeeds] && merge_request.head_pipeline && merge_request.head_pipeline.active? + if params[:merge_when_pipeline_succeeds] && merge_request.head_pipeline && merge_request.head_pipeline.active? ::MergeRequests::MergeWhenPipelineSucceedsService .new(merge_request.target_project, current_user, merge_params) .execute(merge_request) @@ -208,10 +216,10 @@ module API desc 'Cancel merge if "Merge When Pipeline Succeeds" is enabled' do success Entities::MergeRequest end - post ':id/merge_requests/:merge_request_id/cancel_merge_when_build_succeeds' do - merge_request = find_project_merge_request(params[:merge_request_id]) + post ':id/merge_requests/:merge_request_iid/cancel_merge_when_pipeline_succeeds' do + merge_request = find_project_merge_request(params[:merge_request_iid]) - unauthorized! unless merge_request.can_cancel_merge_when_build_succeeds?(current_user) + unauthorized! unless merge_request.can_cancel_merge_when_pipeline_succeeds?(current_user) ::MergeRequest::MergeWhenPipelineSucceedsService .new(merge_request.target_project, current_user) @@ -224,8 +232,8 @@ module API params do use :pagination end - get ':id/merge_requests/:merge_request_id/comments' do - merge_request = find_merge_request_with_access(params[:merge_request_id]) + get ':id/merge_requests/:merge_request_iid/comments' do + merge_request = find_merge_request_with_access(params[:merge_request_iid]) present paginate(merge_request.notes.fresh), with: Entities::MRNote end @@ -235,8 +243,8 @@ module API params do requires :note, type: String, desc: 'The text of the comment' end - post ':id/merge_requests/:merge_request_id/comments' do - merge_request = find_merge_request_with_access(params[:merge_request_id], :create_note) + post ':id/merge_requests/:merge_request_iid/comments' do + merge_request = find_merge_request_with_access(params[:merge_request_iid], :create_note) opts = { note: params[:note], @@ -259,8 +267,8 @@ module API params do use :pagination end - get ':id/merge_requests/:merge_request_id/closes_issues' do - merge_request = find_merge_request_with_access(params[:merge_request_id]) + get ':id/merge_requests/:merge_request_iid/closes_issues' do + merge_request = find_merge_request_with_access(params[:merge_request_iid]) issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user)) present paginate(issues), with: issue_entity(user_project), current_user: current_user end diff --git a/lib/api/milestones.rb b/lib/api/milestones.rb index 0b4ed76b35c..e7ab82f08db 100644 --- a/lib/api/milestones.rb +++ b/lib/api/milestones.rb @@ -23,14 +23,15 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects do + resource :projects, requirements: { id: %r{[^/]+} } do desc 'Get a list of project milestones' do success Entities::Milestone end params do optional :state, type: String, values: %w[active closed all], default: 'all', desc: 'Return "active", "closed", or "all" milestones' - optional :iid, type: Array[Integer], desc: 'The IID of the milestone' + optional :iids, type: Array[Integer], desc: 'The IIDs of the milestones' + optional :search, type: String, desc: 'The search criteria for the title or description of the milestone' use :pagination end get ":id/milestones" do @@ -38,7 +39,8 @@ module API milestones = user_project.milestones milestones = filter_milestones_state(milestones, params[:state]) - milestones = filter_by_iid(milestones, params[:iid]) if params[:iid].present? + milestones = filter_by_iid(milestones, params[:iids]) if params[:iids].present? + milestones = filter_by_search(milestones, params[:search]) if params[:search] present paginate(milestones), with: Entities::Milestone end @@ -101,7 +103,7 @@ module API end desc 'Get all issues for a single project milestone' do - success Entities::Issue + success Entities::IssueBasic end params do requires :milestone_id, type: Integer, desc: 'The ID of a project milestone' @@ -114,16 +116,17 @@ module API finder_params = { project_id: user_project.id, - milestone_title: milestone.title + milestone_title: milestone.title, + sort: 'position_asc' } issues = IssuesFinder.new(current_user, finder_params).execute - present paginate(issues), with: Entities::Issue, current_user: current_user, project: user_project + present paginate(issues), with: Entities::IssueBasic, current_user: current_user, project: user_project end desc 'Get all merge requests for a single project milestone' do detail 'This feature was introduced in GitLab 9.' - success Entities::MergeRequest + success Entities::MergeRequestBasic end params do requires :milestone_id, type: Integer, desc: 'The ID of a project milestone' @@ -136,11 +139,15 @@ module API finder_params = { project_id: user_project.id, - milestone_id: milestone.id + milestone_id: milestone.id, + sort: 'position_asc' } merge_requests = MergeRequestsFinder.new(current_user, finder_params).execute - present paginate(merge_requests), with: Entities::MergeRequest, current_user: current_user, project: user_project + present paginate(merge_requests), + with: Entities::MergeRequestBasic, + current_user: current_user, + project: user_project end end end diff --git a/lib/api/notes.rb b/lib/api/notes.rb index 8beccaaabd1..29ceffdbd2d 100644 --- a/lib/api/notes.rb +++ b/lib/api/notes.rb @@ -4,12 +4,12 @@ module API before { authenticate! } - NOTEABLE_TYPES = [Issue, MergeRequest, Snippet] + NOTEABLE_TYPES = [Issue, MergeRequest, Snippet].freeze params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects do + resource :projects, requirements: { id: %r{[^/]+} } do NOTEABLE_TYPES.each do |noteable_type| noteables_str = noteable_type.to_s.underscore.pluralize @@ -85,7 +85,7 @@ module API note = ::Notes::CreateService.new(user_project, current_user, opts).execute if note.valid? - present note, with: Entities::const_get(note.class.name) + present note, with: Entities.const_get(note.class.name) else not_found!("Note #{note.errors.messages}") end @@ -132,8 +132,6 @@ module API authorize! :admin_note, note ::Notes::DestroyService.new(user_project, current_user).execute(note) - - present note, with: Entities::Note end end end diff --git a/lib/api/notification_settings.rb b/lib/api/notification_settings.rb index c5e9b3ad69b..992ea5dc24d 100644 --- a/lib/api/notification_settings.rb +++ b/lib/api/notification_settings.rb @@ -48,14 +48,14 @@ module API end %w[group project].each do |source_type| - resource source_type.pluralize do + params do + requires :id, type: String, desc: "The #{source_type} ID" + end + resource source_type.pluralize, requirements: { id: %r{[^/]+} } do desc "Get #{source_type} level notification level settings, defaults to Global" do detail 'This feature was introduced in GitLab 8.12' success Entities::NotificationSetting end - params do - requires :id, type: String, desc: 'The group ID or project ID or project NAMESPACE/PROJECT_NAME' - end get ":id/notification_settings" do source = find_source(source_type, params[:id]) @@ -69,7 +69,6 @@ module API success Entities::NotificationSetting end params do - requires :id, type: String, desc: 'The group ID or project ID or project NAMESPACE/PROJECT_NAME' optional :level, type: String, desc: "The #{source_type} notification level" NotificationSetting::EMAIL_EVENTS.each do |event| optional event, type: Boolean, desc: 'Enable/disable this notification' diff --git a/lib/api/pipelines.rb b/lib/api/pipelines.rb index f59f7959173..754c3d85a04 100644 --- a/lib/api/pipelines.rb +++ b/lib/api/pipelines.rb @@ -7,21 +7,21 @@ module API params do requires :id, type: String, desc: 'The project ID' end - resource :projects do + resource :projects, requirements: { id: %r{[^/]+} } do desc 'Get all Pipelines of the project' do detail 'This feature was introduced in GitLab 8.11.' - success Entities::Pipeline + success Entities::PipelineBasic end params do use :pagination - optional :scope, type: String, values: ['running', 'branches', 'tags'], + optional :scope, type: String, values: %w(running branches tags), desc: 'Either running, branches, or tags' end get ':id/pipelines' do authorize! :read_pipeline, user_project pipelines = PipelinesFinder.new(user_project).execute(scope: params[:scope]) - present paginate(pipelines), with: Entities::Pipeline + present paginate(pipelines), with: Entities::PipelineBasic end desc 'Create a new pipeline' do diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb index f7a28d7ad10..53791166c33 100644 --- a/lib/api/project_hooks.rb +++ b/lib/api/project_hooks.rb @@ -24,7 +24,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects do + resource :projects, requirements: { id: %r{[^/]+} } do desc 'Get project hooks' do success Entities::ProjectHook end @@ -90,12 +90,9 @@ module API requires :hook_id, type: Integer, desc: 'The ID of the hook to delete' end delete ":id/hooks/:hook_id" do - begin - present user_project.hooks.destroy(params[:hook_id]), with: Entities::ProjectHook - rescue - # ProjectHook can raise Error if hook_id not found - not_found!("Error deleting hook #{params[:hook_id]}") - end + hook = user_project.hooks.find(params.delete(:hook_id)) + + hook.destroy end end end diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb index 2a1cce73f3f..cfee38a9baf 100644 --- a/lib/api/project_snippets.rb +++ b/lib/api/project_snippets.rb @@ -7,7 +7,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects do + resource :projects, requirements: { id: %r{[^/]+} } do helpers do def handle_project_member_errors(errors) if errors[:project_access].any? @@ -50,11 +50,9 @@ module API requires :title, type: String, desc: 'The title of the snippet' requires :file_name, type: String, desc: 'The file name of the snippet' requires :code, type: String, desc: 'The content of the snippet' - requires :visibility_level, type: Integer, - values: [Gitlab::VisibilityLevel::PRIVATE, - Gitlab::VisibilityLevel::INTERNAL, - Gitlab::VisibilityLevel::PUBLIC], - desc: 'The visibility level of the snippet' + requires :visibility, type: String, + values: Gitlab::VisibilityLevel.string_values, + desc: 'The visibility of the snippet' end post ":id/snippets" do authorize! :create_project_snippet, user_project @@ -80,11 +78,9 @@ module API optional :title, type: String, desc: 'The title of the snippet' optional :file_name, type: String, desc: 'The file name of the snippet' optional :code, type: String, desc: 'The content of the snippet' - optional :visibility_level, type: Integer, - values: [Gitlab::VisibilityLevel::PRIVATE, - Gitlab::VisibilityLevel::INTERNAL, - Gitlab::VisibilityLevel::PUBLIC], - desc: 'The visibility level of the snippet' + optional :visibility, type: String, + values: Gitlab::VisibilityLevel.string_values, + desc: 'The visibility of the snippet' at_least_one_of :title, :file_name, :code, :visibility_level end put ":id/snippets/:snippet_id" do diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 366e5679edd..0fbe1669d45 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -16,13 +16,10 @@ module API optional :shared_runners_enabled, type: Boolean, desc: 'Flag indication if shared runners are enabled for that project' optional :container_registry_enabled, type: Boolean, desc: 'Flag indication if the container registry is enabled for that project' optional :lfs_enabled, type: Boolean, desc: 'Flag indication if Git LFS is enabled for that project' - optional :visibility_level, type: Integer, values: [ - Gitlab::VisibilityLevel::PRIVATE, - Gitlab::VisibilityLevel::INTERNAL, - Gitlab::VisibilityLevel::PUBLIC ], desc: 'Create a public project. The same as visibility_level = 20.' + optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The visibility of the project.' optional :public_builds, type: Boolean, desc: 'Perform public builds' optional :request_access_enabled, type: Boolean, desc: 'Allow users to request member access' - optional :only_allow_merge_if_build_succeeds, type: Boolean, desc: 'Only allow to merge if builds succeed' + optional :only_allow_merge_if_pipeline_succeeds, type: Boolean, desc: 'Only allow to merge if builds succeed' optional :only_allow_merge_if_all_discussions_are_resolved, type: Boolean, desc: 'Only allow to merge if all discussions are resolved' end end @@ -47,11 +44,12 @@ module API params :filter_params do optional :archived, type: Boolean, default: false, desc: 'Limit by archived status' - optional :visibility, type: String, values: %w[public internal private], + optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'Limit by visibility' - optional :search, type: String, desc: 'Return list of authorized projects matching the search criteria' + optional :search, type: String, desc: 'Return list of projects matching the search criteria' optional :owned, type: Boolean, default: false, desc: 'Limit by owned by authenticated user' optional :starred, type: Boolean, default: false, desc: 'Limit by starred status' + optional :membership, type: Boolean, default: false, desc: 'Limit by projects that the current user is a member of' end params :statistics_params do @@ -93,8 +91,9 @@ module API success Entities::Project end params do - requires :name, type: String, desc: 'The name of the project' + optional :name, type: String, desc: 'The name of the project' optional :path, type: String, desc: 'The path of the repository' + at_least_one_of :name, :path use :optional_params use :create_params end @@ -143,7 +142,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects, requirements: { id: /[^\/]+/ } do + resource :projects, requirements: { id: %r{[^/]+} } do desc 'Get a single project' do success Entities::ProjectWithAccess end @@ -206,8 +205,8 @@ module API at_least_one_of :name, :description, :issues_enabled, :merge_requests_enabled, :wiki_enabled, :builds_enabled, :snippets_enabled, :shared_runners_enabled, :container_registry_enabled, - :lfs_enabled, :visibility_level, :public_builds, - :request_access_enabled, :only_allow_merge_if_build_succeeds, + :lfs_enabled, :visibility, :public_builds, + :request_access_enabled, :only_allow_merge_if_pipeline_succeeds, :only_allow_merge_if_all_discussions_are_resolved, :path, :default_branch end @@ -215,7 +214,7 @@ module API authorize_admin_project attrs = declared_params(include_missing: false) authorize! :rename_project, user_project if attrs[:name].present? - authorize! :change_visibility_level, user_project if attrs[:visibility_level].present? + authorize! :change_visibility_level, user_project if attrs[:visibility].present? result = ::Projects::UpdateService.new(user_project, current_user, attrs).execute @@ -281,6 +280,8 @@ module API delete ":id" do authorize! :remove_project, user_project ::Projects::DestroyService.new(user_project, current_user, {}).async_execute + + accepted! end desc 'Mark this project as forked from another' @@ -350,7 +351,6 @@ module API not_found!('Group Link') unless link link.destroy - no_content! end desc 'Upload a file' @@ -374,6 +374,19 @@ module API present paginate(users), with: Entities::UserBasic end + + desc 'Start the housekeeping task for a project' do + detail 'This feature was introduced in GitLab 9.0.' + end + post ':id/housekeeping' do + authorize_admin_project + + begin + ::Projects::HousekeepingService.new(user_project).execute + rescue ::Projects::HousekeepingService::LeaseTaken => error + conflict!(error.message) + end + end end end end diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb index bfda6f45b0a..8f16e532ecb 100644 --- a/lib/api/repositories.rb +++ b/lib/api/repositories.rb @@ -9,7 +9,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects do + resource :projects, requirements: { id: %r{[^/]+} } do helpers do def handle_project_member_errors(errors) if errors[:project_access].any? @@ -17,19 +17,34 @@ module API end not_found! end + + def assign_blob_vars! + authorize! :download_code, user_project + + @repo = user_project.repository + + begin + @blob = Gitlab::Git::Blob.raw(@repo, params[:sha]) + @blob.load_all_data!(@repo) + rescue + not_found! 'Blob' + end + + not_found! 'Blob' unless @blob + end end desc 'Get a project repository tree' do success Entities::RepoTreeObject end params do - optional :ref_name, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used' + optional :ref, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used' optional :path, type: String, desc: 'The path of the tree' optional :recursive, type: Boolean, default: false, desc: 'Used to get a recursive tree' use :pagination end get ':id/repository/tree' do - ref = params[:ref_name] || user_project.try(:default_branch) || 'master' + ref = params[:ref] || user_project.try(:default_branch) || 'master' path = params[:path] || nil commit = user_project.commit(ref) @@ -40,39 +55,29 @@ module API present paginate(entries), with: Entities::RepoTreeObject end - desc 'Get a raw file contents' + desc 'Get raw blob contents from the repository' params do requires :sha, type: String, desc: 'The commit, branch name, or tag name' - requires :filepath, type: String, desc: 'The path to the file to display' end - get [ ":id/repository/blobs/:sha", ":id/repository/commits/:sha/blob" ] do - repo = user_project.repository - - commit = repo.commit(params[:sha]) - not_found! "Commit" unless commit + get ':id/repository/blobs/:sha/raw' do + assign_blob_vars! - blob = Gitlab::Git::Blob.find(repo, commit.id, params[:filepath]) - not_found! "File" unless blob - - send_git_blob repo, blob + send_git_blob @repo, @blob end - desc 'Get a raw blob contents by blob sha' + desc 'Get a blob from the repository' params do requires :sha, type: String, desc: 'The commit, branch name, or tag name' end - get ':id/repository/raw_blobs/:sha' do - repo = user_project.repository - - begin - blob = Gitlab::Git::Blob.raw(repo, params[:sha]) - rescue - not_found! 'Blob' - end - - not_found! 'Blob' unless blob + get ':id/repository/blobs/:sha' do + assign_blob_vars! - send_git_blob repo, blob + { + size: @blob.size, + encoding: "base64", + content: Base64.strict_encode64(@blob.data), + sha: @blob.id + } end desc 'Get an archive of the repository' diff --git a/lib/api/runner.rb b/lib/api/runner.rb new file mode 100644 index 00000000000..4c9db2c8716 --- /dev/null +++ b/lib/api/runner.rb @@ -0,0 +1,264 @@ +module API + class Runner < Grape::API + helpers ::API::Helpers::Runner + + resource :runners do + desc 'Registers a new Runner' do + success Entities::RunnerRegistrationDetails + http_codes [[201, 'Runner was created'], [403, 'Forbidden']] + end + params do + requires :token, type: String, desc: 'Registration token' + optional :description, type: String, desc: %q(Runner's description) + optional :info, type: Hash, desc: %q(Runner's metadata) + optional :locked, type: Boolean, desc: 'Should Runner be locked for current project' + optional :run_untagged, type: Boolean, desc: 'Should Runner handle untagged jobs' + optional :tag_list, type: Array[String], desc: %q(List of Runner's tags) + end + post '/' do + attributes = attributes_for_keys [:description, :locked, :run_untagged, :tag_list] + + runner = + if runner_registration_token_valid? + # Create shared runner. Requires admin access + Ci::Runner.create(attributes.merge(is_shared: true)) + elsif project = Project.find_by(runners_token: params[:token]) + # Create a specific runner for project. + project.runners.create(attributes) + end + + return forbidden! unless runner + + if runner.id + runner.update(get_runner_version_from_params) + present runner, with: Entities::RunnerRegistrationDetails + else + not_found! + end + end + + desc 'Deletes a registered Runner' do + http_codes [[204, 'Runner was deleted'], [403, 'Forbidden']] + end + params do + requires :token, type: String, desc: %q(Runner's authentication token) + end + delete '/' do + authenticate_runner! + Ci::Runner.find_by_token(params[:token]).destroy + end + + desc 'Validates authentication credentials' do + http_codes [[200, 'Credentials are valid'], [403, 'Forbidden']] + end + params do + requires :token, type: String, desc: %q(Runner's authentication token) + end + post '/verify' do + authenticate_runner! + status 200 + end + end + + resource :jobs do + desc 'Request a job' do + success Entities::JobRequest::Response + http_codes [[201, 'Job was scheduled'], + [204, 'No job for Runner'], + [403, 'Forbidden']] + end + params do + requires :token, type: String, desc: %q(Runner's authentication token) + optional :last_update, type: String, desc: %q(Runner's queue last_update token) + optional :info, type: Hash, desc: %q(Runner's metadata) + end + post '/request' do + authenticate_runner! + no_content! unless current_runner.active? + update_runner_info + + if current_runner.is_runner_queue_value_latest?(params[:last_update]) + header 'X-GitLab-Last-Update', params[:last_update] + Gitlab::Metrics.add_event(:build_not_found_cached) + return no_content! + end + + new_update = current_runner.ensure_runner_queue_value + result = ::Ci::RegisterJobService.new(current_runner).execute + + if result.valid? + if result.build + Gitlab::Metrics.add_event(:build_found, + project: result.build.project.path_with_namespace) + present result.build, with: Entities::JobRequest::Response + else + Gitlab::Metrics.add_event(:build_not_found) + header 'X-GitLab-Last-Update', new_update + no_content! + end + else + # We received build that is invalid due to concurrency conflict + Gitlab::Metrics.add_event(:build_invalid) + conflict! + end + end + + desc 'Updates a job' do + http_codes [[200, 'Job was updated'], [403, 'Forbidden']] + end + params do + requires :token, type: String, desc: %q(Runners's authentication token) + requires :id, type: Integer, desc: %q(Job's ID) + optional :trace, type: String, desc: %q(Job's full trace) + optional :state, type: String, desc: %q(Job's status: success, failed) + end + put '/:id' do + job = Ci::Build.find_by_id(params[:id]) + authenticate_job!(job) + + job.update_attributes(trace: params[:trace]) if params[:trace] + + Gitlab::Metrics.add_event(:update_build, + project: job.project.path_with_namespace) + + case params[:state].to_s + when 'success' + job.success + when 'failed' + job.drop + end + end + + desc 'Appends a patch to the job trace' do + http_codes [[202, 'Trace was patched'], + [400, 'Missing Content-Range header'], + [403, 'Forbidden'], + [416, 'Range not satisfiable']] + end + params do + requires :id, type: Integer, desc: %q(Job's ID) + optional :token, type: String, desc: %q(Job's authentication token) + end + patch '/:id/trace' do + job = Ci::Build.find_by_id(params[:id]) + authenticate_job!(job) + + error!('400 Missing header Content-Range', 400) unless request.headers.has_key?('Content-Range') + content_range = request.headers['Content-Range'] + content_range = content_range.split('-') + + current_length = job.trace_length + unless current_length == content_range[0].to_i + return error!('416 Range Not Satisfiable', 416, { 'Range' => "0-#{current_length}" }) + end + + job.append_trace(request.body.read, content_range[0].to_i) + + status 202 + header 'Job-Status', job.status + header 'Range', "0-#{job.trace_length}" + end + + desc 'Authorize artifacts uploading for job' do + http_codes [[200, 'Upload allowed'], + [403, 'Forbidden'], + [405, 'Artifacts support not enabled'], + [413, 'File too large']] + end + params do + requires :id, type: Integer, desc: %q(Job's ID) + optional :token, type: String, desc: %q(Job's authentication token) + optional :filesize, type: Integer, desc: %q(Artifacts filesize) + end + post '/:id/artifacts/authorize' do + not_allowed! unless Gitlab.config.artifacts.enabled + require_gitlab_workhorse! + Gitlab::Workhorse.verify_api_request!(headers) + + job = Ci::Build.find_by_id(params[:id]) + authenticate_job!(job) + forbidden!('Job is not running') unless job.running? + + if params[:filesize] + file_size = params[:filesize].to_i + file_to_large! unless file_size < max_artifacts_size + end + + status 200 + content_type Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE + Gitlab::Workhorse.artifact_upload_ok + end + + desc 'Upload artifacts for job' do + success Entities::JobRequest::Response + http_codes [[201, 'Artifact uploaded'], + [400, 'Bad request'], + [403, 'Forbidden'], + [405, 'Artifacts support not enabled'], + [413, 'File too large']] + end + params do + requires :id, type: Integer, desc: %q(Job's ID) + optional :token, type: String, desc: %q(Job's authentication token) + optional :expire_in, type: String, desc: %q(Specify when artifacts should expire) + optional :file, type: File, desc: %q(Artifact's file) + optional 'file.path', type: String, desc: %q(path to locally stored body (generated by Workhorse)) + optional 'file.name', type: String, desc: %q(real filename as send in Content-Disposition (generated by Workhorse)) + optional 'file.type', type: String, desc: %q(real content type as send in Content-Type (generated by Workhorse)) + optional 'metadata.path', type: String, desc: %q(path to locally stored body (generated by Workhorse)) + optional 'metadata.name', type: String, desc: %q(filename (generated by Workhorse)) + end + post '/:id/artifacts' do + not_allowed! unless Gitlab.config.artifacts.enabled + require_gitlab_workhorse! + + job = Ci::Build.find_by_id(params[:id]) + authenticate_job!(job) + forbidden!('Job is not running!') unless job.running? + + artifacts_upload_path = ArtifactUploader.artifacts_upload_path + artifacts = uploaded_file(:file, artifacts_upload_path) + metadata = uploaded_file(:metadata, artifacts_upload_path) + + bad_request!('Missing artifacts file!') unless artifacts + file_to_large! unless artifacts.size < max_artifacts_size + + job.artifacts_file = artifacts + job.artifacts_metadata = metadata + job.artifacts_expire_in = params['expire_in'] || + Gitlab::CurrentSettings.current_application_settings.default_artifacts_expire_in + + if job.save + present job, with: Entities::JobRequest::Response + else + render_validation_error!(job) + end + end + + desc 'Download the artifacts file for job' do + http_codes [[200, 'Upload allowed'], + [403, 'Forbidden'], + [404, 'Artifact not found']] + end + params do + requires :id, type: Integer, desc: %q(Job's ID) + optional :token, type: String, desc: %q(Job's authentication token) + end + get '/:id/artifacts' do + job = Ci::Build.find_by_id(params[:id]) + authenticate_job!(job) + + artifacts_file = job.artifacts_file + unless artifacts_file.file_storage? + return redirect_to job.artifacts_file.url + end + + unless artifacts_file.exists? + not_found! + end + + present_file!(artifacts_file.path, artifacts_file.filename) + end + end + end +end diff --git a/lib/api/runners.rb b/lib/api/runners.rb index 4fbd4096533..a77c876a749 100644 --- a/lib/api/runners.rb +++ b/lib/api/runners.rb @@ -14,7 +14,7 @@ module API use :pagination end get do - runners = filter_runners(current_user.ci_authorized_runners, params[:scope], without: ['specific', 'shared']) + runners = filter_runners(current_user.ci_authorized_runners, params[:scope], without: %w(specific shared)) present paginate(runners), with: Entities::Runner end @@ -78,16 +78,15 @@ module API delete ':id' do runner = get_runner(params[:id]) authenticate_delete_runner!(runner) - runner.destroy! - present runner, with: Entities::Runner + runner.destroy! end end params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects do + resource :projects, requirements: { id: %r{[^/]+} } do before { authorize_admin_project } desc 'Get runners available for project' do @@ -136,8 +135,6 @@ module API forbidden!("Only one project associated with the runner. Please remove the runner instead") if runner.projects.count == 1 runner_project.destroy - - present runner, with: Entities::Runner end end diff --git a/lib/api/services.rb b/lib/api/services.rb index 1456fe4688b..4e0c9cb1f63 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -107,26 +107,6 @@ module API desc: 'Enable SSL verification for communication' } ], - 'builds-email' => [ - { - required: true, - name: :recipients, - type: String, - desc: 'Comma-separated list of recipient email addresses' - }, - { - required: false, - name: :add_pusher, - type: Boolean, - desc: 'Add pusher to recipients list' - }, - { - required: false, - name: :notify_only_broken_builds, - type: Boolean, - desc: 'Notify only broken builds' - } - ], 'campfire' => [ { required: true, @@ -403,9 +383,9 @@ module API }, { required: false, - name: :notify_only_broken_builds, + name: :notify_only_broken_pipelines, type: Boolean, - desc: 'Notify only broken builds' + desc: 'Notify only broken pipelines' } ], 'pivotaltracker' => [ @@ -422,6 +402,14 @@ module API 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, @@ -542,7 +530,6 @@ module API BambooService, BugzillaService, BuildkiteService, - BuildsEmailService, CampfireService, CustomIssueTrackerService, DroneCiService, @@ -558,12 +545,26 @@ module API SlackSlashCommandsService, PipelinesEmailService, PivotaltrackerService, + PrometheusService, PushoverService, RedmineService, SlackService, MattermostService, TeamcityService, - ].freeze + ] + + if Rails.env.development? + services['mock-ci'] = [ + { + required: true, + name: :mock_service_url, + type: String, + desc: 'URL to the mock service' + } + ] + + service_classes << MockCiService + end trigger_services = { 'mattermost-slash-commands' => [ @@ -582,7 +583,10 @@ module API ] }.freeze - resource :projects do + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects, requirements: { id: %r{[^/]+} } do before { authenticate! } before { authorize_admin_project } @@ -598,7 +602,7 @@ module API desc "Set #{service_slug} service for project" params do service_classes.each do |service| - event_names = service.try(:event_names) || [] + event_names = service.try(:event_names) || next event_names.each do |event_name| services[service.to_param.tr("_", "-")] << { required: false, @@ -641,9 +645,7 @@ module API hash.merge!(key => nil) end - if service.update_attributes(attrs.merge(active: false)) - true - else + unless service.update_attributes(attrs.merge(active: false)) render_api_error!('400 Bad Request', 400) end end @@ -672,7 +674,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects do + resource :projects, requirements: { id: %r{[^/]+} } do desc "Trigger a slash command for #{service_slug}" do detail 'Added in GitLab 8.13' end diff --git a/lib/api/settings.rb b/lib/api/settings.rb index 747ceb4e3e0..d4d3229f0d1 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -21,9 +21,9 @@ module API end params do optional :default_branch_protection, type: Integer, values: [0, 1, 2], desc: 'Determine if developers can push to master' - optional :default_project_visibility, type: Integer, values: Gitlab::VisibilityLevel.values, desc: 'The default project visibility' - optional :default_snippet_visibility, type: Integer, values: Gitlab::VisibilityLevel.values, desc: 'The default snippet visibility' - optional :default_group_visibility, type: Integer, values: Gitlab::VisibilityLevel.values, desc: 'The default group visibility' + optional :default_project_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default project visibility' + optional :default_snippet_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default snippet visibility' + optional :default_group_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default group visibility' optional :restricted_visibility_levels, type: Array[String], desc: 'Selected levels cannot be used by non-admin users for projects or snippets. If the public level is restricted, user profiles are only visible to logged in users.' optional :import_sources, type: Array[String], values: %w[github bitbucket gitlab google_code fogbugz git gitlab_project], desc: 'Enabled sources for code import during project creation. OmniAuth must be configured for GitHub, Bitbucket, and GitLab.com' @@ -56,7 +56,8 @@ module API given shared_runners_enabled: ->(val) { val } do requires :shared_runners_text, type: String, desc: 'Shared runners text ' end - optional :max_artifacts_size, type: Integer, desc: "Set the maximum file size each build's artifacts can have" + optional :max_artifacts_size, type: Integer, desc: "Set the maximum file size for each job's artifacts" + optional :default_artifacts_expire_in, type: String, desc: "Set the default expiration time for each job's artifacts" optional :max_pages_size, type: Integer, desc: 'Maximum size of pages in MB' optional :container_registry_token_expire_delay, type: Integer, desc: 'Authorization token duration (minutes)' optional :metrics_enabled, type: Boolean, desc: 'Enable the InfluxDB metrics' @@ -117,7 +118,9 @@ module API :send_user_confirmation_email, :domain_whitelist, :domain_blacklist_enabled, :after_sign_up_text, :signin_enabled, :require_two_factor_authentication, :home_page_url, :after_sign_out_path, :sign_in_text, :help_page_text, - :shared_runners_enabled, :max_artifacts_size, :max_pages_size, :container_registry_token_expire_delay, + :shared_runners_enabled, :max_artifacts_size, + :default_artifacts_expire_in, :max_pages_size, + :container_registry_token_expire_delay, :metrics_enabled, :sidekiq_throttling_enabled, :recaptcha_enabled, :akismet_enabled, :admin_notification_email, :sentry_enabled, :repository_storage, :repository_checks_enabled, :koding_enabled, :plantuml_enabled, @@ -125,7 +128,9 @@ module API :housekeeping_enabled, :terminal_max_session_time end put "application/settings" do - if current_settings.update_attributes(declared_params(include_missing: false)) + attrs = declared_params(include_missing: false) + + if current_settings.update_attributes(attrs) present current_settings, with: Entities::ApplicationSetting else render_validation_error!(current_settings) diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb index ac03fbd2a3d..b93fdc62808 100644 --- a/lib/api/snippets.rb +++ b/lib/api/snippets.rb @@ -58,10 +58,10 @@ module API requires :title, type: String, desc: 'The title of a snippet' requires :file_name, type: String, desc: 'The name of a snippet file' requires :content, type: String, desc: 'The content of a snippet' - optional :visibility_level, type: Integer, - values: Gitlab::VisibilityLevel.values, - default: Gitlab::VisibilityLevel::INTERNAL, - desc: 'The visibility level of the snippet' + optional :visibility, type: String, + values: Gitlab::VisibilityLevel.string_values, + default: 'internal', + desc: 'The visibility of the snippet' end post do attrs = declared_params(include_missing: false).merge(request: request, api: true) @@ -85,10 +85,10 @@ module API optional :title, type: String, desc: 'The title of a snippet' optional :file_name, type: String, desc: 'The name of a snippet file' optional :content, type: String, desc: 'The content of a snippet' - optional :visibility_level, type: Integer, - values: Gitlab::VisibilityLevel.values, - desc: 'The visibility level of the snippet' - at_least_one_of :title, :file_name, :content, :visibility_level + optional :visibility, type: String, + values: Gitlab::VisibilityLevel.string_values, + desc: 'The visibility of the snippet' + at_least_one_of :title, :file_name, :content, :visibility end put ':id' do snippet = snippets_for_current_user.find_by(id: params.delete(:id)) @@ -118,9 +118,10 @@ module API delete ':id' do snippet = snippets_for_current_user.find_by(id: params.delete(:id)) return not_found!('Snippet') unless snippet + authorize! :destroy_personal_snippet, snippet + snippet.destroy - no_content! end desc 'Get a raw snippet' do diff --git a/lib/api/subscriptions.rb b/lib/api/subscriptions.rb index acf11dbdf26..dbe54d3cd31 100644 --- a/lib/api/subscriptions.rb +++ b/lib/api/subscriptions.rb @@ -3,7 +3,6 @@ module API before { authenticate! } subscribable_types = { - 'merge_request' => proc { |id| find_merge_request_with_access(id, :update_merge_request) }, 'merge_requests' => proc { |id| find_merge_request_with_access(id, :update_merge_request) }, 'issues' => proc { |id| find_project_issue(id) }, 'labels' => proc { |id| find_project_label(id) }, @@ -13,7 +12,7 @@ module API requires :id, type: String, desc: 'The ID of a project' requires :subscribable_id, type: String, desc: 'The ID of a resource' end - resource :projects do + resource :projects, requirements: { id: %r{[^/]+} } do subscribable_types.each do |type, finder| type_singularized = type.singularize entity_class = Entities.const_get(type_singularized.camelcase) diff --git a/lib/api/system_hooks.rb b/lib/api/system_hooks.rb index d038a3fa828..ed7b23b474a 100644 --- a/lib/api/system_hooks.rb +++ b/lib/api/system_hooks.rb @@ -66,7 +66,7 @@ module API hook = SystemHook.find_by(id: params[:id]) not_found!('System hook') unless hook - present hook.destroy, with: Entities::Hook + hook.destroy end end end diff --git a/lib/api/tags.rb b/lib/api/tags.rb index 86759ab882f..c7b1efe0bfa 100644 --- a/lib/api/tags.rb +++ b/lib/api/tags.rb @@ -7,7 +7,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects do + resource :projects, requirements: { id: %r{[^/]+} } do desc 'Get a project repository tags' do success Entities::RepoTag end @@ -66,11 +66,7 @@ module API result = ::Tags::DestroyService.new(user_project, current_user). execute(params[:tag_name]) - if result[:status] == :success - { - tag_name: params[:tag_name] - } - else + if result[:status] != :success render_api_error!(result[:message], result[:return_code]) end end diff --git a/lib/api/time_tracking_endpoints.rb b/lib/api/time_tracking_endpoints.rb index 85b5f7d98b8..05b4b490e27 100644 --- a/lib/api/time_tracking_endpoints.rb +++ b/lib/api/time_tracking_endpoints.rb @@ -5,11 +5,11 @@ module API included do helpers do def issuable_name - declared_params.has_key?(:issue_id) ? 'issue' : 'merge_request' + declared_params.has_key?(:issue_iid) ? 'issue' : 'merge_request' end def issuable_key - "#{issuable_name}_id".to_sym + "#{issuable_name}_iid".to_sym end def update_issuable_key @@ -50,7 +50,7 @@ module API issuable_name = name.end_with?('Issues') ? 'issue' : 'merge_request' issuable_collection_name = issuable_name.pluralize - issuable_key = "#{issuable_name}_id".to_sym + issuable_key = "#{issuable_name}_iid".to_sym desc "Set a time estimate for a project #{issuable_name}" params do diff --git a/lib/api/todos.rb b/lib/api/todos.rb index 0b9650b296c..d1f7e364029 100644 --- a/lib/api/todos.rb +++ b/lib/api/todos.rb @@ -5,22 +5,22 @@ module API before { authenticate! } ISSUABLE_TYPES = { - 'merge_requests' => ->(id) { find_merge_request_with_access(id) }, - 'issues' => ->(id) { find_project_issue(id) } - } + 'merge_requests' => ->(iid) { find_merge_request_with_access(iid) }, + 'issues' => ->(iid) { find_project_issue(iid) } + }.freeze params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects do + resource :projects, requirements: { id: %r{[^/]+} } do ISSUABLE_TYPES.each do |type, finder| - type_id_str = "#{type.singularize}_id".to_sym + type_id_str = "#{type.singularize}_iid".to_sym desc 'Create a todo on an issuable' do success Entities::Todo end params do - requires type_id_str, type: Integer, desc: 'The ID of an issuable' + requires type_id_str, type: Integer, desc: 'The IID of an issuable' end post ":id/#{type}/:#{type_id_str}/todo" do issuable = instance_exec(params[type_id_str], &finder) diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb index 87a717ba751..a9f2ca2608e 100644 --- a/lib/api/triggers.rb +++ b/lib/api/triggers.rb @@ -5,38 +5,33 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects do - desc 'Trigger a GitLab project build' do - success Entities::TriggerRequest + resource :projects, requirements: { id: %r{[^/]+} } do + desc 'Trigger a GitLab project pipeline' do + success Entities::Pipeline end params do requires :ref, type: String, desc: 'The commit sha or name of a branch or tag' requires :token, type: String, desc: 'The unique token of trigger' optional :variables, type: Hash, desc: 'The list of variables to be injected into build' end - post ":id/(ref/:ref/)trigger/builds" do + post ":id/(ref/:ref/)trigger/pipeline", requirements: { ref: /.+/ } do project = find_project(params[:id]) trigger = Ci::Trigger.find_by_token(params[:token].to_s) not_found! unless project && trigger unauthorized! unless trigger.project == project # validate variables - variables = params[:variables] - if variables - unless variables.all? { |key, value| key.is_a?(String) && value.is_a?(String) } - render_api_error!('variables needs to be a map of key-valued strings', 400) - end - - # convert variables from Mash to Hash - variables = variables.to_h + variables = params[:variables].to_h + unless variables.all? { |key, value| key.is_a?(String) && value.is_a?(String) } + render_api_error!('variables needs to be a map of key-valued strings', 400) end # create request and trigger builds trigger_request = Ci::CreateTriggerRequestService.new.execute(project, trigger, params[:ref].to_s, variables) if trigger_request - present trigger_request, with: Entities::TriggerRequest + present trigger_request.pipeline, with: Entities::Pipeline else - errors = 'No builds created' + errors = 'No pipeline created' render_api_error!(errors, 400) end end @@ -60,13 +55,13 @@ module API success Entities::Trigger end params do - requires :token, type: String, desc: 'The unique token of trigger' + requires :trigger_id, type: Integer, desc: 'The trigger ID' end - get ':id/triggers/:token' do + get ':id/triggers/:trigger_id' do authenticate! authorize! :admin_build, user_project - trigger = user_project.triggers.find_by(token: params[:token].to_s) + trigger = user_project.triggers.find(params.delete(:trigger_id)) return not_found!('Trigger') unless trigger present trigger, with: Entities::Trigger @@ -75,31 +70,79 @@ module API desc 'Create a trigger' do success Entities::Trigger end + params do + requires :description, type: String, desc: 'The trigger description' + end post ':id/triggers' do authenticate! authorize! :admin_build, user_project - trigger = user_project.triggers.create + trigger = user_project.triggers.create( + declared_params(include_missing: false).merge(owner: current_user)) - present trigger, with: Entities::Trigger + if trigger.valid? + present trigger, with: Entities::Trigger + else + render_validation_error!(trigger) + end + end + + desc 'Update a trigger' do + success Entities::Trigger + end + params do + requires :trigger_id, type: Integer, desc: 'The trigger ID' + optional :description, type: String, desc: 'The trigger description' + end + put ':id/triggers/:trigger_id' do + authenticate! + authorize! :admin_build, user_project + + trigger = user_project.triggers.find(params.delete(:trigger_id)) + return not_found!('Trigger') unless trigger + + if trigger.update(declared_params(include_missing: false)) + present trigger, with: Entities::Trigger + else + render_validation_error!(trigger) + end + end + + desc 'Take ownership of trigger' do + success Entities::Trigger + end + params do + requires :trigger_id, type: Integer, desc: 'The trigger ID' + end + post ':id/triggers/:trigger_id/take_ownership' do + authenticate! + authorize! :admin_build, user_project + + trigger = user_project.triggers.find(params.delete(:trigger_id)) + return not_found!('Trigger') unless trigger + + if trigger.update(owner: current_user) + status :ok + present trigger, with: Entities::Trigger + else + render_validation_error!(trigger) + end end desc 'Delete a trigger' do success Entities::Trigger end params do - requires :token, type: String, desc: 'The unique token of trigger' + requires :trigger_id, type: Integer, desc: 'The trigger ID' end - delete ':id/triggers/:token' do + delete ':id/triggers/:trigger_id' do authenticate! authorize! :admin_build, user_project - trigger = user_project.triggers.find_by(token: params[:token].to_s) + trigger = user_project.triggers.find(params.delete(:trigger_id)) return not_found!('Trigger') unless trigger trigger.destroy - - present trigger, with: Entities::Trigger end end end diff --git a/lib/api/users.rb b/lib/api/users.rb index fbc17953691..2d4d5a25221 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -9,6 +9,11 @@ module API resource :users, requirements: { uid: /[0-9]*/, id: /[0-9]*/ } do helpers do + def find_user(params) + id = params[:user_id] || params[:id] + User.find_by(id: id) || not_found!('User') + end + params :optional_attributes do optional :skype, type: String, desc: 'The Skype username' optional :linkedin, type: String, desc: 'The LinkedIn username' @@ -40,7 +45,7 @@ module API use :pagination end get do - unless can?(current_user, :read_users_list, nil) + unless can?(current_user, :read_users_list) render_api_error!("Not authorized.", 403) end @@ -172,7 +177,7 @@ module API end end - user_params.merge!(password_expires_at: Time.now) if user_params[:password].present? + user_params[:password_expires_at] = Time.now if user_params[:password].present? if user.update_attributes(user_params.except(:extern_uid, :provider)) present user, with: Entities::UserPublic @@ -236,7 +241,7 @@ module API key = user.keys.find_by(id: params[:key_id]) not_found!('Key') unless key - present key.destroy, with: Entities::SSHKey + key.destroy end desc 'Add an email address to a specified user. Available only for admins.' do @@ -362,6 +367,76 @@ module API present paginate(events), with: Entities::Event end + + params do + requires :user_id, type: Integer, desc: 'The ID of the user' + end + segment ':user_id' do + resource :impersonation_tokens do + helpers do + def finder(options = {}) + user = find_user(params) + PersonalAccessTokensFinder.new({ user: user, impersonation: true }.merge(options)) + end + + def find_impersonation_token + finder.find_by(id: declared_params[:impersonation_token_id]) || not_found!('Impersonation Token') + end + end + + before { authenticated_as_admin! } + + desc 'Retrieve impersonation tokens. Available only for admins.' do + detail 'This feature was introduced in GitLab 9.0' + success Entities::ImpersonationToken + end + params do + use :pagination + optional :state, type: String, default: 'all', values: %w[all active inactive], desc: 'Filters (all|active|inactive) impersonation_tokens' + end + get { present paginate(finder(declared_params(include_missing: false)).execute), with: Entities::ImpersonationToken } + + desc 'Create a impersonation token. Available only for admins.' do + detail 'This feature was introduced in GitLab 9.0' + success Entities::ImpersonationToken + end + params do + requires :name, type: String, desc: 'The name of the impersonation token' + optional :expires_at, type: Date, desc: 'The expiration date in the format YEAR-MONTH-DAY of the impersonation token' + optional :scopes, type: Array, desc: 'The array of scopes of the impersonation token' + end + post do + impersonation_token = finder.build(declared_params(include_missing: false)) + + if impersonation_token.save + present impersonation_token, with: Entities::ImpersonationToken + else + render_validation_error!(impersonation_token) + end + end + + desc 'Retrieve impersonation token. Available only for admins.' do + detail 'This feature was introduced in GitLab 9.0' + success Entities::ImpersonationToken + end + params do + requires :impersonation_token_id, type: Integer, desc: 'The ID of the impersonation token' + end + get ':impersonation_token_id' do + present find_impersonation_token, with: Entities::ImpersonationToken + end + + desc 'Revoke a impersonation token. Available only for admins.' do + detail 'This feature was introduced in GitLab 9.0' + end + params do + requires :impersonation_token_id, type: Integer, desc: 'The ID of the impersonation token' + end + delete ':impersonation_token_id' do + find_impersonation_token.revoke! + end + end + end end resource :user do @@ -422,7 +497,7 @@ module API key = current_user.keys.find_by(id: params[:key_id]) not_found!('Key') unless key - present key.destroy, with: Entities::SSHKey + key.destroy end desc "Get the currently authenticated user's email addresses" do diff --git a/lib/api/v3/award_emoji.rb b/lib/api/v3/award_emoji.rb new file mode 100644 index 00000000000..b96b2d70b12 --- /dev/null +++ b/lib/api/v3/award_emoji.rb @@ -0,0 +1,130 @@ +module API + module V3 + class AwardEmoji < Grape::API + include PaginationParams + + before { authenticate! } + AWARDABLES = %w[issue merge_request snippet].freeze + + resource :projects, requirements: { id: %r{[^/]+} } do + AWARDABLES.each do |awardable_type| + awardable_string = awardable_type.pluralize + awardable_id_string = "#{awardable_type}_id" + + params do + requires :id, type: String, desc: 'The ID of a project' + requires :"#{awardable_id_string}", type: Integer, desc: "The ID of an Issue, Merge Request or Snippet" + end + + [ + ":id/#{awardable_string}/:#{awardable_id_string}/award_emoji", + ":id/#{awardable_string}/:#{awardable_id_string}/notes/:note_id/award_emoji" + ].each do |endpoint| + + desc 'Get a list of project +awardable+ award emoji' do + detail 'This feature was introduced in 8.9' + success Entities::AwardEmoji + end + params do + use :pagination + end + get endpoint do + if can_read_awardable? + awards = awardable.award_emoji + present paginate(awards), with: Entities::AwardEmoji + else + not_found!("Award Emoji") + end + end + + desc 'Get a specific award emoji' do + detail 'This feature was introduced in 8.9' + success Entities::AwardEmoji + end + params do + requires :award_id, type: Integer, desc: 'The ID of the award' + end + get "#{endpoint}/:award_id" do + if can_read_awardable? + present awardable.award_emoji.find(params[:award_id]), with: Entities::AwardEmoji + else + not_found!("Award Emoji") + end + end + + desc 'Award a new Emoji' do + detail 'This feature was introduced in 8.9' + success Entities::AwardEmoji + end + params do + requires :name, type: String, desc: 'The name of a award_emoji (without colons)' + end + post endpoint do + not_found!('Award Emoji') unless can_read_awardable? && can_award_awardable? + + award = awardable.create_award_emoji(params[:name], current_user) + + if award.persisted? + present award, with: Entities::AwardEmoji + else + not_found!("Award Emoji #{award.errors.messages}") + end + end + + desc 'Delete a +awardables+ award emoji' do + detail 'This feature was introduced in 8.9' + success Entities::AwardEmoji + end + params do + requires :award_id, type: Integer, desc: 'The ID of an award emoji' + end + delete "#{endpoint}/:award_id" do + award = awardable.award_emoji.find(params[:award_id]) + + unauthorized! unless award.user == current_user || current_user.admin? + + award.destroy + present award, with: Entities::AwardEmoji + end + end + end + end + + helpers do + def can_read_awardable? + can?(current_user, read_ability(awardable), awardable) + end + + def can_award_awardable? + awardable.user_can_award?(current_user, params[:name]) + end + + def awardable + @awardable ||= + begin + if params.include?(:note_id) + note_id = params.delete(:note_id) + + awardable.notes.find(note_id) + elsif params.include?(:issue_id) + user_project.issues.find(params[:issue_id]) + elsif params.include?(:merge_request_id) + user_project.merge_requests.find(params[:merge_request_id]) + else + user_project.snippets.find(params[:snippet_id]) + end + end + end + + def read_ability(awardable) + case awardable + when Note + read_ability(awardable.noteable) + else + :"read_#{awardable.class.to_s.underscore}" + end + end + end + end + end +end diff --git a/lib/api/v3/boards.rb b/lib/api/v3/boards.rb index 31d708bc2c8..94acc67171e 100644 --- a/lib/api/v3/boards.rb +++ b/lib/api/v3/boards.rb @@ -6,7 +6,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects do + resource :projects, requirements: { id: %r{[^/]+} } do desc 'Get all project boards' do detail 'This feature was introduced in 8.13' success ::API::Entities::Board @@ -44,6 +44,27 @@ module API authorize!(:read_board, user_project) present board_lists, with: ::API::Entities::List end + + desc 'Delete a board list' do + detail 'This feature was introduced in 8.13' + success ::API::Entities::List + end + params do + requires :list_id, type: Integer, desc: 'The ID of a board list' + end + delete "/lists/:list_id" do + authorize!(:admin_list, user_project) + + list = board_lists.find(params[:list_id]) + + service = ::Boards::Lists::DestroyService.new(user_project, current_user) + + if service.execute(list) + present list, with: ::API::Entities::List + else + render_api_error!({ error: 'List could not be deleted!' }, 400) + end + end end end end diff --git a/lib/api/v3/branches.rb b/lib/api/v3/branches.rb index 733c6b21be5..0a877b960f6 100644 --- a/lib/api/v3/branches.rb +++ b/lib/api/v3/branches.rb @@ -9,7 +9,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects do + resource :projects, requirements: { id: %r{[^/]+} } do desc 'Get a project repository branches' do success ::API::Entities::RepoBranch end @@ -18,6 +18,54 @@ module API present branches, with: ::API::Entities::RepoBranch, project: user_project end + + desc 'Delete a branch' + params do + requires :branch, type: String, desc: 'The name of the branch' + end + delete ":id/repository/branches/:branch", requirements: { branch: /.+/ } do + authorize_push_project + + result = DeleteBranchService.new(user_project, current_user). + execute(params[:branch]) + + if result[:status] == :success + status(200) + { + branch_name: params[:branch] + } + else + render_api_error!(result[:message], result[:return_code]) + end + end + + desc 'Delete all merged branches' + delete ":id/repository/merged_branches" do + DeleteMergedBranchesService.new(user_project, current_user).async_execute + + status(200) + end + + desc 'Create branch' do + success ::API::Entities::RepoBranch + end + params do + requires :branch_name, type: String, desc: 'The name of the branch' + requires :ref, type: String, desc: 'Create branch from commit sha or existing branch' + end + post ":id/repository/branches" do + authorize_push_project + result = CreateBranchService.new(user_project, current_user). + execute(params[:branch_name], params[:ref]) + + if result[:status] == :success + present result[:branch], + with: ::API::Entities::RepoBranch, + project: user_project + else + render_api_error!(result[:message], 400) + end + end end end end diff --git a/lib/api/v3/broadcast_messages.rb b/lib/api/v3/broadcast_messages.rb new file mode 100644 index 00000000000..417e4ad0b26 --- /dev/null +++ b/lib/api/v3/broadcast_messages.rb @@ -0,0 +1,31 @@ +module API + module V3 + class BroadcastMessages < Grape::API + include PaginationParams + + before { authenticate! } + before { authenticated_as_admin! } + + resource :broadcast_messages do + helpers do + def find_message + BroadcastMessage.find(params[:id]) + end + end + + desc 'Delete a broadcast message' do + detail 'This feature was introduced in GitLab 8.12.' + success ::API::Entities::BroadcastMessage + end + params do + requires :id, type: Integer, desc: 'Broadcast message ID' + end + delete ':id' do + message = find_message + + present message.destroy, with: ::API::Entities::BroadcastMessage + end + end + end + end +end diff --git a/lib/api/v3/builds.rb b/lib/api/v3/builds.rb new file mode 100644 index 00000000000..6f97102c6ef --- /dev/null +++ b/lib/api/v3/builds.rb @@ -0,0 +1,255 @@ +module API + module V3 + class Builds < Grape::API + include PaginationParams + + before { authenticate! } + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects do + helpers do + params :optional_scope do + optional :scope, types: [String, Array[String]], desc: 'The scope of builds to show', + values: %w(pending running failed success canceled skipped), + coerce_with: ->(scope) { + if scope.is_a?(String) + [scope] + elsif scope.is_a?(Hashie::Mash) + scope.values + else + ['unknown'] + end + } + end + end + + desc 'Get a project builds' do + success ::API::V3::Entities::Build + end + params do + use :optional_scope + use :pagination + end + get ':id/builds' do + builds = user_project.builds.order('id DESC') + builds = filter_builds(builds, params[:scope]) + + present paginate(builds), with: ::API::V3::Entities::Build + end + + desc 'Get builds for a specific commit of a project' do + success ::API::V3::Entities::Build + end + params do + requires :sha, type: String, desc: 'The SHA id of a commit' + use :optional_scope + use :pagination + end + get ':id/repository/commits/:sha/builds' do + authorize_read_builds! + + return not_found! unless user_project.commit(params[:sha]) + + pipelines = user_project.pipelines.where(sha: params[:sha]) + builds = user_project.builds.where(pipeline: pipelines).order('id DESC') + builds = filter_builds(builds, params[:scope]) + + present paginate(builds), with: ::API::V3::Entities::Build + end + + desc 'Get a specific build of a project' do + success ::API::V3::Entities::Build + end + params do + requires :build_id, type: Integer, desc: 'The ID of a build' + end + get ':id/builds/:build_id' do + authorize_read_builds! + + build = get_build!(params[:build_id]) + + present build, with: ::API::V3::Entities::Build + end + + desc 'Download the artifacts file from build' do + detail 'This feature was introduced in GitLab 8.5' + end + params do + requires :build_id, type: Integer, desc: 'The ID of a build' + end + get ':id/builds/:build_id/artifacts' do + authorize_read_builds! + + build = get_build!(params[:build_id]) + + present_artifacts!(build.artifacts_file) + end + + desc 'Download the artifacts file from build' do + detail 'This feature was introduced in GitLab 8.10' + end + params do + requires :ref_name, type: String, desc: 'The ref from repository' + requires :job, type: String, desc: 'The name for the build' + end + get ':id/builds/artifacts/:ref_name/download', + requirements: { ref_name: /.+/ } do + authorize_read_builds! + + builds = user_project.latest_successful_builds_for(params[:ref_name]) + latest_build = builds.find_by!(name: params[:job]) + + present_artifacts!(latest_build.artifacts_file) + end + + # TODO: We should use `present_file!` and leave this implementation for backward compatibility (when build trace + # is saved in the DB instead of file). But before that, we need to consider how to replace the value of + # `runners_token` with some mask (like `xxxxxx`) when sending trace file directly by workhorse. + desc 'Get a trace of a specific build of a project' + params do + requires :build_id, type: Integer, desc: 'The ID of a build' + end + get ':id/builds/:build_id/trace' do + authorize_read_builds! + + build = get_build!(params[:build_id]) + + header 'Content-Disposition', "infile; filename=\"#{build.id}.log\"" + content_type 'text/plain' + env['api.format'] = :binary + + trace = build.trace + body trace + end + + desc 'Cancel a specific build of a project' do + success ::API::V3::Entities::Build + end + params do + requires :build_id, type: Integer, desc: 'The ID of a build' + end + post ':id/builds/:build_id/cancel' do + authorize_update_builds! + + build = get_build!(params[:build_id]) + + build.cancel + + present build, with: ::API::V3::Entities::Build + end + + desc 'Retry a specific build of a project' do + success ::API::V3::Entities::Build + end + params do + requires :build_id, type: Integer, desc: 'The ID of a build' + end + post ':id/builds/:build_id/retry' do + authorize_update_builds! + + build = get_build!(params[:build_id]) + return forbidden!('Build is not retryable') unless build.retryable? + + build = Ci::Build.retry(build, current_user) + + present build, with: ::API::V3::Entities::Build + end + + desc 'Erase build (remove artifacts and build trace)' do + success ::API::V3::Entities::Build + end + params do + requires :build_id, type: Integer, desc: 'The ID of a build' + end + post ':id/builds/:build_id/erase' do + authorize_update_builds! + + build = get_build!(params[:build_id]) + return forbidden!('Build is not erasable!') unless build.erasable? + + build.erase(erased_by: current_user) + present build, with: ::API::V3::Entities::Build + end + + desc 'Keep the artifacts to prevent them from being deleted' do + success ::API::V3::Entities::Build + end + params do + requires :build_id, type: Integer, desc: 'The ID of a build' + end + post ':id/builds/:build_id/artifacts/keep' do + authorize_update_builds! + + build = get_build!(params[:build_id]) + return not_found!(build) unless build.artifacts? + + build.keep_artifacts! + + status 200 + present build, with: ::API::V3::Entities::Build + end + + desc 'Trigger a manual build' do + success ::API::V3::Entities::Build + detail 'This feature was added in GitLab 8.11' + end + params do + requires :build_id, type: Integer, desc: 'The ID of a Build' + end + post ":id/builds/:build_id/play" do + authorize_read_builds! + + build = get_build!(params[:build_id]) + + bad_request!("Unplayable Job") unless build.playable? + + build.play(current_user) + + status 200 + present build, with: ::API::V3::Entities::Build + end + end + + helpers do + def get_build(id) + user_project.builds.find_by(id: id.to_i) + end + + def get_build!(id) + get_build(id) || not_found! + end + + def present_artifacts!(artifacts_file) + if !artifacts_file.file_storage? + redirect_to(build.artifacts_file.url) + elsif artifacts_file.exists? + present_file!(artifacts_file.path, artifacts_file.filename) + else + not_found! + end + end + + def filter_builds(builds, scope) + return builds if scope.nil? || scope.empty? + + available_statuses = ::CommitStatus::AVAILABLE_STATUSES + + unknown = scope - available_statuses + render_api_error!('Scope contains invalid value(s)', 400) unless unknown.empty? + + builds.where(status: available_statuses && scope) + end + + def authorize_read_builds! + authorize! :read_build, user_project + end + + def authorize_update_builds! + authorize! :update_build, user_project + end + end + end + end +end diff --git a/lib/api/v3/commits.rb b/lib/api/v3/commits.rb index 477e22fd25e..3414a2883e5 100644 --- a/lib/api/v3/commits.rb +++ b/lib/api/v3/commits.rb @@ -11,7 +11,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects do + resource :projects, requirements: { id: %r{[^/]+} } do desc 'Get a project repository commits' do success ::API::Entities::RepoCommit end @@ -55,13 +55,6 @@ module API branch = attrs.delete(:branch_name) attrs.merge!(branch: branch, start_branch: branch, target_branch: branch) - attrs[:actions].map! do |action| - action[:action] = action[:action].to_sym - action[:file_path].slice!(0) if action[:file_path] && action[:file_path].start_with?('/') - action[:previous_path].slice!(0) if action[:previous_path] && action[:previous_path].start_with?('/') - action - end - result = ::Files::MultiService.new(user_project, current_user, attrs).execute if result[:status] == :success @@ -137,9 +130,7 @@ module API commit_params = { commit: commit, - create_merge_request: false, - source_project: user_project, - source_branch: commit.cherry_pick_branch_name, + start_branch: params[:branch], target_branch: params[:branch] } @@ -162,7 +153,7 @@ module API optional :path, type: String, desc: 'The file path' given :path do requires :line, type: Integer, desc: 'The line number' - requires :line_type, type: String, values: ['new', 'old'], default: 'new', desc: 'The type of the line' + requires :line_type, type: String, values: %w(new old), default: 'new', desc: 'The type of the line' end end post ':id/repository/commits/:sha/comments' do diff --git a/lib/api/v3/deploy_keys.rb b/lib/api/v3/deploy_keys.rb index 5bbb167755c..bbb174b6003 100644 --- a/lib/api/v3/deploy_keys.rb +++ b/lib/api/v3/deploy_keys.rb @@ -13,7 +13,7 @@ module API params do requires :id, type: String, desc: 'The ID of the project' end - resource :projects do + resource :projects, requirements: { id: %r{[^/]+} } do before { authorize_admin_project } %w(keys deploy_keys).each do |path| diff --git a/lib/api/v3/deployments.rb b/lib/api/v3/deployments.rb new file mode 100644 index 00000000000..1d4972eda26 --- /dev/null +++ b/lib/api/v3/deployments.rb @@ -0,0 +1,43 @@ +module API + module V3 + # Deployments RESTful API endpoints + class Deployments < Grape::API + include PaginationParams + + before { authenticate! } + + params do + requires :id, type: String, desc: 'The project ID' + end + resource :projects, requirements: { id: %r{[^/]+} } do + desc 'Get all deployments of the project' do + detail 'This feature was introduced in GitLab 8.11.' + success ::API::V3::Deployments + end + params do + use :pagination + end + get ':id/deployments' do + authorize! :read_deployment, user_project + + present paginate(user_project.deployments), with: ::API::V3::Deployments + end + + desc 'Gets a specific deployment' do + detail 'This feature was introduced in GitLab 8.11.' + success ::API::V3::Deployments + end + params do + requires :deployment_id, type: Integer, desc: 'The deployment ID' + end + get ':id/deployments/:deployment_id' do + authorize! :read_deployment, user_project + + deployment = user_project.deployments.find(params[:deployment_id]) + + present deployment, with: ::API::V3::Deployments + end + end + end + end +end diff --git a/lib/api/v3/entities.rb b/lib/api/v3/entities.rb index 3cc0dc968a8..832b4bdeb4f 100644 --- a/lib/api/v3/entities.rb +++ b/lib/api/v3/entities.rb @@ -11,6 +11,243 @@ module API Gitlab::UrlBuilder.build(snippet) end end + + class Note < Grape::Entity + expose :id + expose :note, as: :body + expose :attachment_identifier, as: :attachment + expose :author, using: ::API::Entities::UserBasic + expose :created_at, :updated_at + expose :system?, as: :system + expose :noteable_id, :noteable_type + # upvote? and downvote? are deprecated, always return false + expose(:upvote?) { |note| false } + expose(:downvote?) { |note| false } + end + + class Event < Grape::Entity + expose :title, :project_id, :action_name + expose :target_id, :target_type, :author_id + expose :data, :target_title + expose :created_at + expose :note, using: Entities::Note, if: ->(event, options) { event.note? } + expose :author, using: ::API::Entities::UserBasic, if: ->(event, options) { event.author } + + expose :author_username do |event, options| + event.author&.username + end + end + + class AwardEmoji < Grape::Entity + expose :id + expose :name + expose :user, using: ::API::Entities::UserBasic + expose :created_at, :updated_at + expose :awardable_id, :awardable_type + end + + class Project < Grape::Entity + expose :id, :description, :default_branch, :tag_list + expose :public?, as: :public + expose :archived?, as: :archived + expose :visibility_level, :ssh_url_to_repo, :http_url_to_repo, :web_url + expose :owner, using: ::API::Entities::UserBasic, unless: ->(project, options) { project.group } + expose :name, :name_with_namespace + expose :path, :path_with_namespace + expose :container_registry_enabled + + # Expose old field names with the new permissions methods to keep API compatible + expose(:issues_enabled) { |project, options| project.feature_available?(:issues, options[:current_user]) } + expose(:merge_requests_enabled) { |project, options| project.feature_available?(:merge_requests, options[:current_user]) } + expose(:wiki_enabled) { |project, options| project.feature_available?(:wiki, options[:current_user]) } + expose(:builds_enabled) { |project, options| project.feature_available?(:builds, options[:current_user]) } + expose(:snippets_enabled) { |project, options| project.feature_available?(:snippets, options[:current_user]) } + + expose :created_at, :last_activity_at + expose :shared_runners_enabled + expose :lfs_enabled?, as: :lfs_enabled + expose :creator_id + expose :namespace, using: 'API::Entities::Namespace' + expose :forked_from_project, using: ::API::Entities::BasicProjectDetails, if: lambda{ |project, options| project.forked? } + expose :avatar_url + expose :star_count, :forks_count + expose :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[:current_user]) && project.default_issues_tracker? } + expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] } + expose :public_builds + expose :shared_with_groups do |project, options| + ::API::Entities::SharedGroup.represent(project.project_group_links.all, options) + end + expose :only_allow_merge_if_pipeline_succeeds, as: :only_allow_merge_if_build_succeeds + expose :request_access_enabled + expose :only_allow_merge_if_all_discussions_are_resolved + + expose :statistics, using: '::API::V3::Entities::ProjectStatistics', if: :statistics + end + + class ProjectWithAccess < Project + expose :permissions do + expose :project_access, using: ::API::Entities::ProjectAccess do |project, options| + project.project_members.find_by(user_id: options[:current_user].id) + end + + expose :group_access, using: ::API::Entities::GroupAccess do |project, options| + if project.group + project.group.group_members.find_by(user_id: options[:current_user].id) + end + end + end + end + + class MergeRequest < Grape::Entity + expose :id, :iid + expose(:project_id) { |entity| entity.project.id } + expose :title, :description + expose :state, :created_at, :updated_at + expose :target_branch, :source_branch + expose :upvotes, :downvotes + expose :author, :assignee, using: ::API::Entities::UserBasic + expose :source_project_id, :target_project_id + expose :label_names, as: :labels + expose :work_in_progress?, as: :work_in_progress + expose :milestone, using: ::API::Entities::Milestone + expose :merge_when_pipeline_succeeds, as: :merge_when_build_succeeds + expose :merge_status + expose :diff_head_sha, as: :sha + expose :merge_commit_sha + expose :subscribed do |merge_request, options| + merge_request.subscribed?(options[:current_user], options[:project]) + end + expose :user_notes_count + expose :should_remove_source_branch?, as: :should_remove_source_branch + expose :force_remove_source_branch?, as: :force_remove_source_branch + + expose :web_url do |merge_request, options| + Gitlab::UrlBuilder.build(merge_request) + end + end + + class Group < Grape::Entity + expose :id, :name, :path, :description, :visibility_level + expose :lfs_enabled?, as: :lfs_enabled + expose :avatar_url + expose :web_url + expose :request_access_enabled + expose :full_name, :full_path + expose :parent_id + + expose :statistics, if: :statistics do + with_options format_with: -> (value) { value.to_i } do + expose :storage_size + expose :repository_size + expose :lfs_objects_size + expose :build_artifacts_size + end + end + end + + class GroupDetail < Group + expose :projects, using: Entities::Project + expose :shared_projects, using: Entities::Project + end + + class ApplicationSetting < Grape::Entity + expose :id + expose :default_projects_limit + expose :signup_enabled + expose :signin_enabled + expose :gravatar_enabled + expose :sign_in_text + expose :after_sign_up_text + expose :created_at + expose :updated_at + expose :home_page_url + expose :default_branch_protection + expose :restricted_visibility_levels + expose :max_attachment_size + expose :session_expire_delay + expose :default_project_visibility + expose :default_snippet_visibility + expose :default_group_visibility + expose :domain_whitelist + expose :domain_blacklist_enabled + expose :domain_blacklist + expose :user_oauth_applications + expose :after_sign_out_path + expose :container_registry_token_expire_delay + expose :repository_storage + expose :repository_storages + expose :koding_enabled + expose :koding_url + expose :plantuml_enabled + expose :plantuml_url + expose :terminal_max_session_time + end + + class Environment < ::API::Entities::EnvironmentBasic + expose :project, using: Entities::Project + end + + class Trigger < Grape::Entity + expose :token, :created_at, :updated_at, :deleted_at, :last_used + expose :owner, using: ::API::Entities::UserBasic + end + + class TriggerRequest < Grape::Entity + expose :id, :variables + end + + class Build < Grape::Entity + expose :id, :status, :stage, :name, :ref, :tag, :coverage + expose :created_at, :started_at, :finished_at + expose :user, with: ::API::Entities::User + expose :artifacts_file, using: ::API::Entities::JobArtifactFile, if: -> (build, opts) { build.artifacts? } + expose :commit, with: ::API::Entities::RepoCommit + expose :runner, with: ::API::Entities::Runner + expose :pipeline, with: ::API::Entities::PipelineBasic + end + + class BuildArtifactFile < Grape::Entity + expose :filename, :size + end + + class Deployment < Grape::Entity + expose :id, :iid, :ref, :sha, :created_at + expose :user, using: ::API::Entities::UserBasic + expose :environment, using: ::API::Entities::EnvironmentBasic + expose :deployable, using: Entities::Build + end + + class MergeRequestChanges < MergeRequest + expose :diffs, as: :changes, using: ::API::Entities::RepoDiff do |compare, _| + compare.raw_diffs(all_diffs: true).to_a + end + end + + class ProjectStatistics < Grape::Entity + expose :commit_count + expose :storage_size + expose :repository_size + expose :lfs_objects_size + expose :build_artifacts_size + end + + class ProjectService < Grape::Entity + expose :id, :title, :created_at, :updated_at, :active + expose :push_events, :issues_events, :merge_requests_events + expose :tag_push_events, :note_events, :build_events, :pipeline_events + # Expose serialized properties + expose :properties do |service, options| + field_names = service.fields. + select { |field| options[:include_passwords] || field[:type] != 'password' }. + map { |field| field[:name] } + service.properties.slice(*field_names) + end + end + + class ProjectHook < ::API::Entities::Hook + expose :project_id, :issues_events, :merge_requests_events + expose :note_events, :build_events, :pipeline_events, :wiki_page_events + end end end end diff --git a/lib/api/v3/environments.rb b/lib/api/v3/environments.rb new file mode 100644 index 00000000000..6bb4e016a01 --- /dev/null +++ b/lib/api/v3/environments.rb @@ -0,0 +1,87 @@ +module API + module V3 + class Environments < Grape::API + include ::API::Helpers::CustomValidators + include PaginationParams + + before { authenticate! } + + params do + requires :id, type: String, desc: 'The project ID' + end + resource :projects, requirements: { id: %r{[^/]+} } do + desc 'Get all environments of the project' do + detail 'This feature was introduced in GitLab 8.11.' + success Entities::Environment + end + params do + use :pagination + end + get ':id/environments' do + authorize! :read_environment, user_project + + present paginate(user_project.environments), with: Entities::Environment + end + + desc 'Creates a new environment' do + detail 'This feature was introduced in GitLab 8.11.' + success Entities::Environment + end + params do + requires :name, type: String, desc: 'The name of the environment to be created' + optional :external_url, type: String, desc: 'URL on which this deployment is viewable' + optional :slug, absence: { message: "is automatically generated and cannot be changed" } + end + post ':id/environments' do + authorize! :create_environment, user_project + + environment = user_project.environments.create(declared_params) + + if environment.persisted? + present environment, with: Entities::Environment + else + render_validation_error!(environment) + end + end + + desc 'Updates an existing environment' do + detail 'This feature was introduced in GitLab 8.11.' + success Entities::Environment + end + params do + requires :environment_id, type: Integer, desc: 'The environment ID' + optional :name, type: String, desc: 'The new environment name' + optional :external_url, type: String, desc: 'The new URL on which this deployment is viewable' + optional :slug, absence: { message: "is automatically generated and cannot be changed" } + end + put ':id/environments/:environment_id' do + authorize! :update_environment, user_project + + environment = user_project.environments.find(params[:environment_id]) + + update_params = declared_params(include_missing: false).extract!(:name, :external_url) + if environment.update(update_params) + present environment, with: Entities::Environment + else + render_validation_error!(environment) + end + end + + desc 'Deletes an existing environment' do + detail 'This feature was introduced in GitLab 8.11.' + success Entities::Environment + end + params do + requires :environment_id, type: Integer, desc: 'The environment ID' + end + delete ':id/environments/:environment_id' do + authorize! :update_environment, user_project + + environment = user_project.environments.find(params[:environment_id]) + + present environment.destroy, with: Entities::Environment + end + end + end + end +end diff --git a/lib/api/v3/files.rb b/lib/api/v3/files.rb index 4f8d58d37c8..13542b0c71c 100644 --- a/lib/api/v3/files.rb +++ b/lib/api/v3/files.rb @@ -40,7 +40,7 @@ module API params do requires :id, type: String, desc: 'The project ID' end - resource :projects do + resource :projects, requirements: { id: %r{[^/]+} } do desc 'Get a file from repository' params do requires :file_path, type: String, desc: 'The path to the file. Ex. lib/class.rb' diff --git a/lib/api/v3/groups.rb b/lib/api/v3/groups.rb new file mode 100644 index 00000000000..c5b37622d79 --- /dev/null +++ b/lib/api/v3/groups.rb @@ -0,0 +1,181 @@ +module API + module V3 + class Groups < Grape::API + include PaginationParams + + before { authenticate! } + + helpers do + params :optional_params do + optional :description, type: String, desc: 'The description of the group' + optional :visibility_level, type: Integer, desc: 'The visibility level of the group' + optional :lfs_enabled, type: Boolean, desc: 'Enable/disable LFS for the projects in this group' + optional :request_access_enabled, type: Boolean, desc: 'Allow users to request member access' + end + + params :statistics_params do + optional :statistics, type: Boolean, default: false, desc: 'Include project statistics' + end + + def present_groups(groups, options = {}) + options = options.reverse_merge( + with: Entities::Group, + current_user: current_user, + ) + + groups = groups.with_statistics if options[:statistics] + present paginate(groups), options + end + end + + resource :groups do + desc 'Get a groups list' do + success Entities::Group + end + params do + use :statistics_params + optional :skip_groups, type: Array[Integer], desc: 'Array of group ids to exclude from list' + optional :all_available, type: Boolean, desc: 'Show all group that you have access to' + optional :search, type: String, desc: 'Search for a specific group' + optional :order_by, type: String, values: %w[name path], default: 'name', desc: 'Order by name or path' + optional :sort, type: String, values: %w[asc desc], default: 'asc', desc: 'Sort by asc (ascending) or desc (descending)' + use :pagination + end + get do + groups = if current_user.admin + Group.all + elsif params[:all_available] + GroupsFinder.new.execute(current_user) + else + current_user.groups + end + + groups = groups.search(params[:search]) if params[:search].present? + groups = groups.where.not(id: params[:skip_groups]) if params[:skip_groups].present? + groups = groups.reorder(params[:order_by] => params[:sort]) + + present_groups groups, statistics: params[:statistics] && current_user.is_admin? + end + + desc 'Get list of owned groups for authenticated user' do + success Entities::Group + end + params do + use :pagination + use :statistics_params + end + get '/owned' do + present_groups current_user.owned_groups, statistics: params[:statistics] + end + + desc 'Create a group. Available only for users who can create groups.' do + success Entities::Group + end + params do + requires :name, type: String, desc: 'The name of the group' + requires :path, type: String, desc: 'The path of the group' + optional :parent_id, type: Integer, desc: 'The parent group id for creating nested group' + use :optional_params + end + post do + authorize! :create_group + + group = ::Groups::CreateService.new(current_user, declared_params(include_missing: false)).execute + + if group.persisted? + present group, with: Entities::Group, current_user: current_user + else + render_api_error!("Failed to save group #{group.errors.messages}", 400) + end + end + end + + params do + requires :id, type: String, desc: 'The ID of a group' + end + resource :groups, requirements: { id: %r{[^/]+} } do + desc 'Update a group. Available only for users who can administrate groups.' do + success Entities::Group + end + params do + optional :name, type: String, desc: 'The name of the group' + optional :path, type: String, desc: 'The path of the group' + use :optional_params + at_least_one_of :name, :path, :description, :visibility_level, + :lfs_enabled, :request_access_enabled + 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 + present group, with: Entities::GroupDetail, current_user: current_user + else + render_validation_error!(group) + end + end + + desc 'Get a single group, with containing projects.' do + success Entities::GroupDetail + end + get ":id" do + group = find_group!(params[:id]) + present group, with: Entities::GroupDetail, current_user: current_user + end + + desc 'Remove a group.' + delete ":id" do + group = find_group!(params[:id]) + authorize! :admin_group, group + present ::Groups::DestroyService.new(group, current_user).execute, with: Entities::GroupDetail, current_user: current_user + end + + desc 'Get a list of projects in this group.' do + success Entities::Project + end + params do + optional :archived, type: Boolean, default: false, desc: 'Limit by archived status' + optional :visibility, type: String, values: %w[public internal private], + desc: 'Limit by visibility' + optional :search, type: String, desc: 'Return list of authorized projects matching the search criteria' + optional :order_by, type: String, values: %w[id name path created_at updated_at last_activity_at], + default: 'created_at', desc: 'Return projects ordered by field' + optional :sort, type: String, values: %w[asc desc], default: 'desc', + desc: 'Return projects sorted in ascending and descending order' + optional :simple, type: Boolean, default: false, + desc: 'Return only the ID, URL, name, and path of each project' + optional :owned, type: Boolean, default: false, desc: 'Limit by owned by authenticated user' + optional :starred, type: Boolean, default: false, desc: 'Limit by starred status' + + use :pagination + end + get ":id/projects" do + group = find_group!(params[:id]) + projects = GroupProjectsFinder.new(group).execute(current_user) + projects = filter_projects(projects) + entity = params[:simple] ? ::API::Entities::BasicProjectDetails : Entities::Project + present paginate(projects), with: entity, current_user: current_user + end + + desc 'Transfer a project to the group namespace. Available only for admin.' do + success Entities::GroupDetail + end + params do + requires :project_id, type: String, desc: 'The ID or path of the project' + end + post ":id/projects/:project_id", requirements: { project_id: /.+/ } do + authenticated_as_admin! + group = find_group!(params[:id]) + project = find_project!(params[:project_id]) + result = ::Projects::TransferService.new(project, current_user).execute(group) + + if result + present group, with: Entities::GroupDetail, current_user: current_user + else + render_api_error!("Failed to transfer project #{project.errors.messages}", 400) + end + end + end + end + end +end diff --git a/lib/api/v3/helpers.rb b/lib/api/v3/helpers.rb new file mode 100644 index 00000000000..0f234d4cdad --- /dev/null +++ b/lib/api/v3/helpers.rb @@ -0,0 +1,19 @@ +module API + module V3 + module Helpers + def find_project_issue(id) + IssuesFinder.new(current_user, project_id: user_project.id).find(id) + end + + def find_project_merge_request(id) + MergeRequestsFinder.new(current_user, project_id: user_project.id).find(id) + end + + def find_merge_request_with_access(id, access_level = :read_merge_request) + merge_request = user_project.merge_requests.find(id) + authorize! access_level, merge_request + merge_request + end + end + end +end diff --git a/lib/api/v3/issues.rb b/lib/api/v3/issues.rb index d0af09f0e1e..54c6a8060b8 100644 --- a/lib/api/v3/issues.rb +++ b/lib/api/v3/issues.rb @@ -68,7 +68,7 @@ module API params do requires :id, type: String, desc: 'The ID of a group' end - resource :groups do + resource :groups, requirements: { id: %r{[^/]+} } do desc 'Get a list of group issues' do success ::API::Entities::Issue end @@ -89,7 +89,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects do + resource :projects, requirements: { id: %r{[^/]+} } do include TimeTrackingEndpoints desc 'Get a list of project issues' do @@ -103,7 +103,7 @@ module API use :issues_params end get ":id/issues" do - project = find_project(params[:id]) + project = find_project!(params[:id]) issues = find_issues(project_id: project.id) @@ -139,12 +139,7 @@ module API end issue_params = declared_params(include_missing: false) - - if merge_request_iid = params[:merge_request_for_resolving_discussions] - issue_params[:merge_request_for_resolving_discussions] = MergeRequestsFinder.new(current_user, project_id: user_project.id). - execute. - find_by(iid: merge_request_iid) - end + issue_params = issue_params.merge(merge_request_to_resolve_discussions_of: issue_params.delete(:merge_request_for_resolving_discussions)) issue = ::Issues::CreateService.new(user_project, current_user, @@ -226,6 +221,8 @@ module API not_found!('Issue') unless issue authorize!(:destroy_issue, issue) + + status(200) issue.destroy end end diff --git a/lib/api/v3/labels.rb b/lib/api/v3/labels.rb index 5c3261311bf..bd5eb2175e8 100644 --- a/lib/api/v3/labels.rb +++ b/lib/api/v3/labels.rb @@ -6,13 +6,28 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects do + resource :projects, requirements: { id: %r{[^/]+} } do desc 'Get all labels of the project' do success ::API::Entities::Label end get ':id/labels' do present available_labels, with: ::API::Entities::Label, current_user: current_user, project: user_project end + + desc 'Delete an existing label' do + success ::API::Entities::Label + end + params do + requires :name, type: String, desc: 'The name of the label to be deleted' + end + delete ':id/labels' do + authorize! :admin_label, user_project + + label = user_project.labels.find_by(title: params[:name]) + not_found!('Label') unless label + + present label.destroy, with: ::API::Entities::Label, current_user: current_user, project: user_project + end end end end diff --git a/lib/api/v3/members.rb b/lib/api/v3/members.rb index 4e6cb2e3c52..684860b553e 100644 --- a/lib/api/v3/members.rb +++ b/lib/api/v3/members.rb @@ -11,7 +11,7 @@ module API params do requires :id, type: String, desc: "The #{source_type} ID" end - resource source_type.pluralize do + resource source_type.pluralize, requirements: { id: %r{[^/]+} } do desc 'Gets a list of group or project members viewable by the authenticated user.' do success ::API::Entities::Member end @@ -86,13 +86,12 @@ module API optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY' end put ":id/members/:user_id" do - source = find_source(source_type, params[:id]) + source = find_source(source_type, params.delete(:id)) authorize_admin_source!(source_type, source) - member = source.members.find_by!(user_id: params[:user_id]) - attrs = attributes_for_keys [:access_level, :expires_at] + member = source.members.find_by!(user_id: params.delete(:user_id)) - if member.update_attributes(attrs) + if member.update_attributes(declared_params(include_missing: false)) present member.user, with: ::API::Entities::Member, member: member else # This is to ensure back-compatibility but 400 behavior should be used @@ -120,6 +119,7 @@ module API # This is to ensure back-compatibility but 204 behavior should be used # for all DELETE endpoints in 9.0! if member.nil? + status(200 ) { message: "Access revoked", id: params[:user_id].to_i } else ::Members::DestroyService.new(source, current_user, declared_params).execute diff --git a/lib/api/v3/merge_request_diffs.rb b/lib/api/v3/merge_request_diffs.rb new file mode 100644 index 00000000000..35f462e907b --- /dev/null +++ b/lib/api/v3/merge_request_diffs.rb @@ -0,0 +1,44 @@ +module API + module V3 + # MergeRequestDiff API + class MergeRequestDiffs < Grape::API + before { authenticate! } + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects, requirements: { id: %r{[^/]+} } do + desc 'Get a list of merge request diff versions' do + detail 'This feature was introduced in GitLab 8.12.' + success ::API::Entities::MergeRequestDiff + end + + params do + requires :merge_request_id, type: Integer, desc: 'The ID of a merge request' + end + + get ":id/merge_requests/:merge_request_id/versions" do + merge_request = find_merge_request_with_access(params[:merge_request_id]) + + present merge_request.merge_request_diffs, with: ::API::Entities::MergeRequestDiff + end + + desc 'Get a single merge request diff version' do + detail 'This feature was introduced in GitLab 8.12.' + success ::API::Entities::MergeRequestDiffFull + end + + params do + requires :merge_request_id, type: Integer, desc: 'The ID of a merge request' + requires :version_id, type: Integer, desc: 'The ID of a merge request diff version' + end + + get ":id/merge_requests/:merge_request_id/versions/:version_id" do + merge_request = find_merge_request_with_access(params[:merge_request_id]) + + present merge_request.merge_request_diffs.find(params[:version_id]), with: ::API::Entities::MergeRequestDiffFull + end + end + end + end +end diff --git a/lib/api/v3/merge_requests.rb b/lib/api/v3/merge_requests.rb index 129f9d850e9..3077240e650 100644 --- a/lib/api/v3/merge_requests.rb +++ b/lib/api/v3/merge_requests.rb @@ -10,7 +10,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects do + resource :projects, requirements: { id: %r{[^/]+} } do include TimeTrackingEndpoints helpers do @@ -28,6 +28,14 @@ module API render_api_error!(errors, 400) end + def issue_entity(project) + if project.has_external_issue_tracker? + ::API::Entities::ExternalIssue + else + ::API::Entities::Issue + end + end + 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' @@ -39,7 +47,7 @@ module API desc 'List merge requests' do detail 'iid filter is deprecated have been removed on V4' - success ::API::Entities::MergeRequest + success ::API::V3::Entities::MergeRequest end params do optional :state, type: String, values: %w[opened closed merged all], default: 'all', @@ -66,11 +74,11 @@ module API end merge_requests = merge_requests.reorder(params[:order_by] => params[:sort]) - present paginate(merge_requests), with: ::API::Entities::MergeRequest, current_user: current_user, project: user_project + present paginate(merge_requests), with: ::API::V3::Entities::MergeRequest, current_user: current_user, project: user_project end desc 'Create a merge request' do - success ::API::Entities::MergeRequest + success ::API::V3::Entities::MergeRequest end params do requires :title, type: String, desc: 'The title of the merge request' @@ -89,7 +97,7 @@ module API merge_request = ::MergeRequests::CreateService.new(user_project, current_user, mr_params).execute if merge_request.valid? - present merge_request, with: ::API::Entities::MergeRequest, current_user: current_user, project: user_project + present merge_request, with: ::API::V3::Entities::MergeRequest, current_user: current_user, project: user_project else handle_merge_request_errors! merge_request.errors end @@ -103,6 +111,8 @@ module API merge_request = find_project_merge_request(params[:merge_request_id]) authorize!(:destroy_merge_request, merge_request) + + status(200) merge_request.destroy end @@ -114,12 +124,12 @@ module API if status == :deprecated detail DEPRECATION_MESSAGE end - success ::API::Entities::MergeRequest + success ::API::V3::Entities::MergeRequest end get path do merge_request = find_merge_request_with_access(params[:merge_request_id]) - present merge_request, with: ::API::Entities::MergeRequest, current_user: current_user, project: user_project + present merge_request, with: ::API::V3::Entities::MergeRequest, current_user: current_user, project: user_project end desc 'Get the commits of a merge request' do @@ -141,7 +151,7 @@ module API end desc 'Update a merge request' do - success ::API::Entities::MergeRequest + success ::API::V3::Entities::MergeRequest end params do optional :title, type: String, allow_blank: false, desc: 'The title of the merge request' @@ -162,21 +172,21 @@ module API merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, mr_params).execute(merge_request) if merge_request.valid? - present merge_request, with: ::API::Entities::MergeRequest, current_user: current_user, project: user_project + present merge_request, with: ::API::V3::Entities::MergeRequest, current_user: current_user, project: user_project else handle_merge_request_errors! merge_request.errors end end desc 'Merge a merge request' do - success ::API::Entities::MergeRequest + success ::API::V3::Entities::MergeRequest end params do optional :merge_commit_message, type: String, desc: 'Custom merge commit message' optional :should_remove_source_branch, type: Boolean, desc: 'When true, the source branch will be deleted if possible' optional :merge_when_build_succeeds, type: Boolean, - desc: 'When true, this merge request will be merged when the pipeline succeeds' + desc: 'When true, this merge request will be merged when the build succeeds' optional :sha, type: String, desc: 'When present, must have the HEAD SHA of the source branch' end put "#{path}/merge" do @@ -209,16 +219,16 @@ module API .execute(merge_request) end - present merge_request, with: ::API::Entities::MergeRequest, current_user: current_user, project: user_project + present merge_request, with: ::API::V3::Entities::MergeRequest, current_user: current_user, project: user_project end - desc 'Cancel merge if "Merge When Pipeline Succeeds" is enabled' do - success ::API::Entities::MergeRequest + desc 'Cancel merge if "Merge When Build succeeds" is enabled' do + success ::API::V3::Entities::MergeRequest end post "#{path}/cancel_merge_when_build_succeeds" do merge_request = find_project_merge_request(params[:merge_request_id]) - unauthorized! unless merge_request.can_cancel_merge_when_build_succeeds?(current_user) + unauthorized! unless merge_request.can_cancel_merge_when_pipeline_succeeds?(current_user) ::MergeRequest::MergeWhenPipelineSucceedsService .new(merge_request.target_project, current_user) diff --git a/lib/api/v3/milestones.rb b/lib/api/v3/milestones.rb new file mode 100644 index 00000000000..be90cec4afc --- /dev/null +++ b/lib/api/v3/milestones.rb @@ -0,0 +1,64 @@ +module API + module V3 + class Milestones < Grape::API + include PaginationParams + + before { authenticate! } + + helpers do + def filter_milestones_state(milestones, state) + case state + when 'active' then milestones.active + when 'closed' then milestones.closed + else milestones + end + end + end + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects, requirements: { id: %r{[^/]+} } do + desc 'Get a list of project milestones' do + success ::API::Entities::Milestone + end + params do + optional :state, type: String, values: %w[active closed all], default: 'all', + desc: 'Return "active", "closed", or "all" milestones' + optional :iid, type: Array[Integer], desc: 'The IID of the milestone' + use :pagination + end + get ":id/milestones" do + authorize! :read_milestone, user_project + + milestones = user_project.milestones + milestones = filter_milestones_state(milestones, params[:state]) + milestones = filter_by_iid(milestones, params[:iid]) if params[:iid].present? + + present paginate(milestones), with: ::API::Entities::Milestone + end + + desc 'Get all issues for a single project milestone' do + success ::API::Entities::Issue + end + params do + requires :milestone_id, type: Integer, desc: 'The ID of a project milestone' + use :pagination + end + get ':id/milestones/:milestone_id/issues' do + authorize! :read_milestone, user_project + + milestone = user_project.milestones.find(params[:milestone_id]) + + finder_params = { + project_id: user_project.id, + milestone_title: milestone.title + } + + issues = IssuesFinder.new(current_user, finder_params).execute + present paginate(issues), with: ::API::Entities::Issue, current_user: current_user, project: user_project + end + end + end + end +end diff --git a/lib/api/v3/notes.rb b/lib/api/v3/notes.rb new file mode 100644 index 00000000000..4f8e0eff4ff --- /dev/null +++ b/lib/api/v3/notes.rb @@ -0,0 +1,148 @@ +module API + module V3 + class Notes < Grape::API + include PaginationParams + + before { authenticate! } + + NOTEABLE_TYPES = [Issue, MergeRequest, Snippet].freeze + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects, requirements: { id: %r{[^/]+} } do + NOTEABLE_TYPES.each do |noteable_type| + noteables_str = noteable_type.to_s.underscore.pluralize + + desc 'Get a list of project +noteable+ notes' do + success ::API::V3::Entities::Note + end + params do + requires :noteable_id, type: Integer, desc: 'The ID of the noteable' + use :pagination + end + get ":id/#{noteables_str}/:noteable_id/notes" do + noteable = user_project.send(noteables_str.to_sym).find(params[:noteable_id]) + + if can?(current_user, noteable_read_ability_name(noteable), noteable) + # We exclude notes that are cross-references and that cannot be viewed + # by the current user. By doing this exclusion at this level and not + # at the DB query level (which we cannot in that case), the current + # page can have less elements than :per_page even if + # there's more than one page. + notes = + # paginate() only works with a relation. This could lead to a + # mismatch between the pagination headers info and the actual notes + # array returned, but this is really a edge-case. + paginate(noteable.notes). + reject { |n| n.cross_reference_not_visible_for?(current_user) } + present notes, with: ::API::V3::Entities::Note + else + not_found!("Notes") + end + end + + desc 'Get a single +noteable+ note' do + success ::API::V3::Entities::Note + end + params do + requires :note_id, type: Integer, desc: 'The ID of a note' + requires :noteable_id, type: Integer, desc: 'The ID of the noteable' + end + get ":id/#{noteables_str}/:noteable_id/notes/:note_id" do + noteable = user_project.send(noteables_str.to_sym).find(params[:noteable_id]) + note = noteable.notes.find(params[:note_id]) + can_read_note = can?(current_user, noteable_read_ability_name(noteable), noteable) && !note.cross_reference_not_visible_for?(current_user) + + if can_read_note + present note, with: ::API::V3::Entities::Note + else + not_found!("Note") + end + end + + desc 'Create a new +noteable+ note' do + success ::API::V3::Entities::Note + end + params do + requires :noteable_id, type: Integer, desc: 'The ID of the noteable' + requires :body, type: String, desc: 'The content of a note' + optional :created_at, type: String, desc: 'The creation date of the note' + end + post ":id/#{noteables_str}/:noteable_id/notes" do + opts = { + note: params[:body], + noteable_type: noteables_str.classify, + noteable_id: params[:noteable_id] + } + + noteable = user_project.send(noteables_str.to_sym).find(params[:noteable_id]) + + if can?(current_user, noteable_read_ability_name(noteable), noteable) + if params[:created_at] && (current_user.is_admin? || user_project.owner == current_user) + opts[:created_at] = params[:created_at] + end + + note = ::Notes::CreateService.new(user_project, current_user, opts).execute + if note.valid? + present note, with: ::API::V3::Entities.const_get(note.class.name) + else + not_found!("Note #{note.errors.messages}") + end + else + not_found!("Note") + end + end + + desc 'Update an existing +noteable+ note' do + success ::API::V3::Entities::Note + end + params do + requires :noteable_id, type: Integer, desc: 'The ID of the noteable' + requires :note_id, type: Integer, desc: 'The ID of a note' + requires :body, type: String, desc: 'The content of a note' + end + put ":id/#{noteables_str}/:noteable_id/notes/:note_id" do + note = user_project.notes.find(params[:note_id]) + + authorize! :admin_note, note + + opts = { + note: params[:body] + } + + note = ::Notes::UpdateService.new(user_project, current_user, opts).execute(note) + + if note.valid? + present note, with: ::API::V3::Entities::Note + else + render_api_error!("Failed to save note #{note.errors.messages}", 400) + end + end + + desc 'Delete a +noteable+ note' do + success ::API::V3::Entities::Note + end + params do + requires :noteable_id, type: Integer, desc: 'The ID of the noteable' + requires :note_id, type: Integer, desc: 'The ID of a note' + end + delete ":id/#{noteables_str}/:noteable_id/notes/:note_id" do + note = user_project.notes.find(params[:note_id]) + authorize! :admin_note, note + + ::Notes::DestroyService.new(user_project, current_user).execute(note) + + present note, with: ::API::V3::Entities::Note + end + end + end + + helpers do + def noteable_read_ability_name(noteable) + "read_#{noteable.class.to_s.underscore}".to_sym + end + end + end + end +end diff --git a/lib/api/v3/pipelines.rb b/lib/api/v3/pipelines.rb new file mode 100644 index 00000000000..82827249244 --- /dev/null +++ b/lib/api/v3/pipelines.rb @@ -0,0 +1,36 @@ +module API + module V3 + class Pipelines < Grape::API + include PaginationParams + + before { authenticate! } + + params do + requires :id, type: String, desc: 'The project ID' + end + resource :projects, requirements: { id: %r{[^/]+} } do + desc 'Get all Pipelines of the project' do + detail 'This feature was introduced in GitLab 8.11.' + success ::API::Entities::Pipeline + end + params do + use :pagination + optional :scope, type: String, values: %w(running branches tags), + desc: 'Either running, branches, or tags' + end + get ':id/pipelines' do + authorize! :read_pipeline, user_project + + pipelines = PipelinesFinder.new(user_project).execute(scope: params[:scope]) + present paginate(pipelines), with: ::API::Entities::Pipeline + end + end + + helpers do + def pipeline + @pipeline ||= user_project.pipelines.find(params[:pipeline_id]) + end + end + end + end +end diff --git a/lib/api/v3/project_hooks.rb b/lib/api/v3/project_hooks.rb new file mode 100644 index 00000000000..94614bfc8b6 --- /dev/null +++ b/lib/api/v3/project_hooks.rb @@ -0,0 +1,106 @@ +module API + module V3 + class ProjectHooks < Grape::API + include PaginationParams + + before { authenticate! } + before { authorize_admin_project } + + helpers do + params :project_hook_properties do + requires :url, type: String, desc: "The URL to send the request to" + optional :push_events, type: Boolean, desc: "Trigger hook on push events" + optional :issues_events, type: Boolean, desc: "Trigger hook on issues events" + optional :merge_requests_events, type: Boolean, desc: "Trigger hook on merge request events" + optional :tag_push_events, type: Boolean, desc: "Trigger hook on tag push events" + optional :note_events, type: Boolean, desc: "Trigger hook on note(comment) events" + optional :build_events, type: Boolean, desc: "Trigger hook on build events" + optional :pipeline_events, type: Boolean, desc: "Trigger hook on pipeline events" + optional :wiki_page_events, type: Boolean, desc: "Trigger hook on wiki events" + optional :enable_ssl_verification, type: Boolean, desc: "Do SSL verification when triggering the hook" + optional :token, type: String, desc: "Secret token to validate received payloads; this will not be returned in the response" + end + end + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects, requirements: { id: %r{[^/]+} } do + desc 'Get project hooks' do + success ::API::V3::Entities::ProjectHook + end + params do + use :pagination + end + get ":id/hooks" do + hooks = paginate user_project.hooks + + present hooks, with: ::API::V3::Entities::ProjectHook + end + + desc 'Get a project hook' do + success ::API::V3::Entities::ProjectHook + end + params do + requires :hook_id, type: Integer, desc: 'The ID of a project hook' + end + get ":id/hooks/:hook_id" do + hook = user_project.hooks.find(params[:hook_id]) + present hook, with: ::API::V3::Entities::ProjectHook + end + + desc 'Add hook to project' do + success ::API::V3::Entities::ProjectHook + end + params do + use :project_hook_properties + end + post ":id/hooks" do + hook = user_project.hooks.new(declared_params(include_missing: false)) + + if hook.save + present hook, with: ::API::V3::Entities::ProjectHook + else + error!("Invalid url given", 422) if hook.errors[:url].present? + + not_found!("Project hook #{hook.errors.messages}") + end + end + + desc 'Update an existing project hook' do + success ::API::V3::Entities::ProjectHook + end + params do + requires :hook_id, type: Integer, desc: "The ID of the hook to update" + use :project_hook_properties + end + put ":id/hooks/:hook_id" do + hook = user_project.hooks.find(params.delete(:hook_id)) + + if hook.update_attributes(declared_params(include_missing: false)) + present hook, with: ::API::V3::Entities::ProjectHook + else + error!("Invalid url given", 422) if hook.errors[:url].present? + + not_found!("Project hook #{hook.errors.messages}") + end + end + + desc 'Deletes project hook' do + success ::API::V3::Entities::ProjectHook + end + params do + requires :hook_id, type: Integer, desc: 'The ID of the hook to delete' + end + delete ":id/hooks/:hook_id" do + begin + present user_project.hooks.destroy(params[:hook_id]), with: ::API::V3::Entities::ProjectHook + rescue + # ProjectHook can raise Error if hook_id not found + not_found!("Error deleting hook #{params[:hook_id]}") + end + end + end + end + end +end diff --git a/lib/api/v3/project_snippets.rb b/lib/api/v3/project_snippets.rb index e03e941d30b..fc065a22d74 100644 --- a/lib/api/v3/project_snippets.rb +++ b/lib/api/v3/project_snippets.rb @@ -8,7 +8,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects do + resource :projects, requirements: { id: %r{[^/]+} } do helpers do def handle_project_member_errors(errors) if errors[:project_access].any? @@ -121,6 +121,8 @@ module API authorize! :admin_project_snippet, snippet snippet.destroy + + status(200) end desc 'Get a raw project snippet' diff --git a/lib/api/v3/projects.rb b/lib/api/v3/projects.rb index 6796da83f07..b753dbab381 100644 --- a/lib/api/v3/projects.rb +++ b/lib/api/v3/projects.rb @@ -5,6 +5,10 @@ module API before { authenticate_non_get! } + after_validation do + set_only_allow_merge_if_pipeline_succeeds! + end + helpers do params :optional_params do optional :description, type: String, desc: 'The description of the project' @@ -20,10 +24,12 @@ module API optional :visibility_level, type: Integer, values: [ Gitlab::VisibilityLevel::PRIVATE, Gitlab::VisibilityLevel::INTERNAL, - Gitlab::VisibilityLevel::PUBLIC ], desc: 'Create a public project. The same as visibility_level = 20.' + Gitlab::VisibilityLevel::PUBLIC + ], desc: 'Create a public project. The same as visibility_level = 20.' optional :public_builds, type: Boolean, desc: 'Perform public builds' optional :request_access_enabled, type: Boolean, desc: 'Allow users to request member access' optional :only_allow_merge_if_build_succeeds, type: Boolean, desc: 'Only allow to merge if builds succeed' + optional :only_allow_merge_if_pipeline_succeeds, type: Boolean, desc: 'Only allow to merge if builds succeed' optional :only_allow_merge_if_all_discussions_are_resolved, type: Boolean, desc: 'Only allow to merge if all discussions are resolved' end @@ -36,6 +42,12 @@ module API end attrs end + + def set_only_allow_merge_if_pipeline_succeeds! + if params.has_key?(:only_allow_merge_if_build_succeeds) + params[:only_allow_merge_if_pipeline_succeeds] = params.delete(:only_allow_merge_if_build_succeeds) + end + end end resource :projects do @@ -74,7 +86,7 @@ module API def present_projects(projects, options = {}) options = options.reverse_merge( - with: ::API::Entities::Project, + with: ::API::V3::Entities::Project, current_user: current_user, simple: params[:simple], ) @@ -94,7 +106,7 @@ module API use :collection_params end get '/visible' do - entity = current_user ? ::API::Entities::ProjectWithAccess : ::API::Entities::BasicProjectDetails + entity = current_user ? ::API::V3::Entities::ProjectWithAccess : ::API::Entities::BasicProjectDetails present_projects ProjectsFinder.new.execute(current_user), with: entity end @@ -108,7 +120,7 @@ module API authenticate! present_projects current_user.authorized_projects, - with: ::API::Entities::ProjectWithAccess + with: ::API::V3::Entities::ProjectWithAccess end desc 'Get an owned projects list for authenticated user' do @@ -122,7 +134,7 @@ module API authenticate! present_projects current_user.owned_projects, - with: ::API::Entities::ProjectWithAccess, + with: ::API::V3::Entities::ProjectWithAccess, statistics: params[:statistics] end @@ -148,11 +160,11 @@ module API get '/all' do authenticated_as_admin! - present_projects Project.all, with: ::API::Entities::ProjectWithAccess, statistics: params[:statistics] + present_projects Project.all, with: ::API::V3::Entities::ProjectWithAccess, statistics: params[:statistics] end desc 'Search for projects the current user has access to' do - success ::API::Entities::Project + success ::API::V3::Entities::Project end params do requires :query, type: String, desc: 'The project name to be searched' @@ -164,15 +176,16 @@ module API projects = search_service.objects('projects', params[:page]) projects = projects.reorder(params[:order_by] => params[:sort]) - present paginate(projects), with: ::API::Entities::Project + present paginate(projects), with: ::API::V3::Entities::Project end desc 'Create new project' do - success ::API::Entities::Project + success ::API::V3::Entities::Project end params do - requires :name, type: String, desc: 'The name of the project' + optional :name, type: String, desc: 'The name of the project' optional :path, type: String, desc: 'The path of the repository' + at_least_one_of :name, :path use :optional_params use :create_params end @@ -181,7 +194,7 @@ module API project = ::Projects::CreateService.new(current_user, attrs).execute if project.saved? - present project, with: ::API::Entities::Project, + present project, with: ::API::V3::Entities::Project, user_can_admin_project: can?(current_user, :admin_project, project) else if project.errors[:limit_reached].present? @@ -192,7 +205,7 @@ module API end desc 'Create new project for a specified user. Only available to admin users.' do - success ::API::Entities::Project + success ::API::V3::Entities::Project end params do requires :name, type: String, desc: 'The name of the project' @@ -210,7 +223,7 @@ module API project = ::Projects::CreateService.new(user, attrs).execute if project.saved? - present project, with: ::API::Entities::Project, + present project, with: ::API::V3::Entities::Project, user_can_admin_project: can?(current_user, :admin_project, project) else render_validation_error!(project) @@ -221,28 +234,28 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects, requirements: { id: /[^\/]+/ } do + resource :projects, requirements: { id: %r{[^/]+} } do desc 'Get a single project' do - success ::API::Entities::ProjectWithAccess + success ::API::V3::Entities::ProjectWithAccess end get ":id" do - entity = current_user ? ::API::Entities::ProjectWithAccess : ::API::Entities::BasicProjectDetails + entity = current_user ? ::API::V3::Entities::ProjectWithAccess : ::API::Entities::BasicProjectDetails present user_project, with: entity, current_user: current_user, user_can_admin_project: can?(current_user, :admin_project, user_project) end desc 'Get events for a single project' do - success ::API::Entities::Event + success ::API::V3::Entities::Event end params do use :pagination end get ":id/events" do - present paginate(user_project.events.recent), with: ::API::Entities::Event + present paginate(user_project.events.recent), with: ::API::V3::Entities::Event end desc 'Fork new project for the current user or provided namespace.' do - success ::API::Entities::Project + success ::API::V3::Entities::Project end params do optional :namespace, type: String, desc: 'The ID or name of the namespace that the project will be forked into' @@ -268,13 +281,13 @@ module API if forked_project.errors.any? conflict!(forked_project.errors.messages) else - present forked_project, with: ::API::Entities::Project, + present forked_project, with: ::API::V3::Entities::Project, user_can_admin_project: can?(current_user, :admin_project, forked_project) end end desc 'Update an existing project' do - success ::API::Entities::Project + success ::API::V3::Entities::Project end params do optional :name, type: String, desc: 'The name of the project' @@ -298,7 +311,7 @@ module API result = ::Projects::UpdateService.new(user_project, current_user, attrs).execute if result[:status] == :success - present user_project, with: ::API::Entities::Project, + present user_project, with: ::API::V3::Entities::Project, user_can_admin_project: can?(current_user, :admin_project, user_project) else render_validation_error!(user_project) @@ -306,29 +319,29 @@ module API end desc 'Archive a project' do - success ::API::Entities::Project + success ::API::V3::Entities::Project end post ':id/archive' do authorize!(:archive_project, user_project) user_project.archive! - present user_project, with: ::API::Entities::Project + present user_project, with: ::API::V3::Entities::Project end desc 'Unarchive a project' do - success ::API::Entities::Project + success ::API::V3::Entities::Project end post ':id/unarchive' do authorize!(:archive_project, user_project) user_project.unarchive! - present user_project, with: ::API::Entities::Project + present user_project, with: ::API::V3::Entities::Project end desc 'Star a project' do - success ::API::Entities::Project + success ::API::V3::Entities::Project end post ':id/star' do if current_user.starred?(user_project) @@ -337,19 +350,19 @@ module API current_user.toggle_star(user_project) user_project.reload - present user_project, with: ::API::Entities::Project + present user_project, with: ::API::V3::Entities::Project end end desc 'Unstar a project' do - success ::API::Entities::Project + success ::API::V3::Entities::Project end delete ':id/star' do if current_user.starred?(user_project) current_user.toggle_star(user_project) user_project.reload - present user_project, with: ::API::Entities::Project + present user_project, with: ::API::V3::Entities::Project else not_modified! end @@ -358,6 +371,8 @@ module API desc 'Remove a project' delete ":id" do authorize! :remove_project, user_project + + status(200) ::Projects::DestroyService.new(user_project, current_user, {}).async_execute end @@ -383,6 +398,7 @@ module API authorize! :remove_fork_project, user_project if user_project.forked? + status(200) user_project.forked_project_link.destroy else not_modified! diff --git a/lib/api/v3/repositories.rb b/lib/api/v3/repositories.rb index 3549ea225ef..e4d14bc8168 100644 --- a/lib/api/v3/repositories.rb +++ b/lib/api/v3/repositories.rb @@ -8,7 +8,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects do + resource :projects, requirements: { id: %r{[^/]+} } do helpers do def handle_project_member_errors(errors) if errors[:project_access].any? @@ -38,6 +38,60 @@ module API present tree.sorted_entries, with: ::API::Entities::RepoTreeObject end + desc 'Get a raw file contents' + params do + requires :sha, type: String, desc: 'The commit, branch name, or tag name' + requires :filepath, type: String, desc: 'The path to the file to display' + end + get [":id/repository/blobs/:sha", ":id/repository/commits/:sha/blob"] do + repo = user_project.repository + commit = repo.commit(params[:sha]) + not_found! "Commit" unless commit + blob = Gitlab::Git::Blob.find(repo, commit.id, params[:filepath]) + not_found! "File" unless blob + send_git_blob repo, blob + end + + desc 'Get a raw blob contents by blob sha' + params do + requires :sha, type: String, desc: 'The commit, branch name, or tag name' + end + get ':id/repository/raw_blobs/:sha' do + repo = user_project.repository + begin + blob = Gitlab::Git::Blob.raw(repo, params[:sha]) + rescue + not_found! 'Blob' + end + not_found! 'Blob' unless blob + send_git_blob repo, blob + end + + desc 'Get an archive of the repository' + params do + optional :sha, type: String, desc: 'The commit sha of the archive to be downloaded' + optional :format, type: String, desc: 'The archive format' + end + get ':id/repository/archive', requirements: { format: Gitlab::Regex.archive_formats_regex } do + begin + send_git_archive user_project.repository, ref: params[:sha], format: params[:format] + rescue + not_found!('File') + end + end + + desc 'Compare two branches, tags, or commits' do + success ::API::Entities::Compare + end + params do + requires :from, type: String, desc: 'The commit, branch name, or tag name to start comparison' + requires :to, type: String, desc: 'The commit, branch name, or tag name to stop comparison' + end + get ':id/repository/compare' do + compare = Gitlab::Git::Compare.new(user_project.repository.raw_repository, params[:from], params[:to]) + present compare, with: ::API::Entities::Compare + end + desc 'Get repository contributors' do success ::API::Entities::Contributor end diff --git a/lib/api/v3/runners.rb b/lib/api/v3/runners.rb new file mode 100644 index 00000000000..1934d6e578c --- /dev/null +++ b/lib/api/v3/runners.rb @@ -0,0 +1,65 @@ +module API + module V3 + class Runners < Grape::API + include PaginationParams + + before { authenticate! } + + resource :runners do + desc 'Remove a runner' do + success ::API::Entities::Runner + end + params do + requires :id, type: Integer, desc: 'The ID of the runner' + end + delete ':id' do + runner = Ci::Runner.find(params[:id]) + not_found!('Runner') unless runner + + authenticate_delete_runner!(runner) + + status(200) + runner.destroy + end + end + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects, requirements: { id: %r{[^/]+} } do + before { authorize_admin_project } + + desc "Disable project's runner" do + success ::API::Entities::Runner + end + params do + requires :runner_id, type: Integer, desc: 'The ID of the runner' + end + delete ':id/runners/:runner_id' do + runner_project = user_project.runner_projects.find_by(runner_id: params[:runner_id]) + not_found!('Runner') unless runner_project + + runner = runner_project.runner + forbidden!("Only one project associated with the runner. Please remove the runner instead") if runner.projects.count == 1 + + runner_project.destroy + + present runner, with: ::API::Entities::Runner + end + end + + helpers do + def authenticate_delete_runner!(runner) + return if current_user.is_admin? + forbidden!("Runner is shared") if runner.is_shared? + forbidden!("Runner associated with more than one project") if runner.projects.count > 1 + forbidden!("No access granted") unless user_can_access_runner?(runner) + end + + def user_can_access_runner?(runner) + current_user.ci_authorized_runners.exists?(runner.id) + end + end + end + end +end diff --git a/lib/api/v3/services.rb b/lib/api/v3/services.rb new file mode 100644 index 00000000000..3bacaeee032 --- /dev/null +++ b/lib/api/v3/services.rb @@ -0,0 +1,644 @@ +module API + module V3 + class Services < Grape::API + 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' + } + ], + 'builds-email' => [ + { + required: true, + name: :recipients, + type: String, + desc: 'Comma-separated list of recipient email addresses' + }, + { + required: false, + name: :add_pusher, + type: Boolean, + desc: 'Add pusher to recipients list' + }, + { + required: false, + name: :notify_only_broken_builds, + type: Boolean, + desc: 'Notify only broken builds' + } + ], + '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' + } + ], + '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' + } + ], + 'gemnasium' => [ + { + required: true, + name: :api_key, + type: String, + desc: 'Your personal API key on gemnasium.com' + }, + { + required: true, + name: :token, + type: String, + desc: "The project's slug on gemnasium.com" + } + ], + '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 URL to the JIRA project which is being linked to this GitLab project, e.g., https://jira.example.com' + }, + { + required: true, + name: :project_key, + type: String, + desc: 'The short identifier for your JIRA project, all uppercase, e.g., PROJ' + }, + { + required: false, + name: :username, + type: String, + desc: 'The username of the user created to be used with GitLab/JIRA' + }, + { + required: false, + 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: Integer, + 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' + } + ], + 'pipelines-email' => [ + { + required: true, + name: :recipients, + type: String, + desc: 'Comma-separated list of recipient email addresses' + }, + { + required: false, + name: :notify_only_broken_builds, + type: Boolean, + desc: 'Notify only broken builds' + } + ], + '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.' + } + ], + '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' + } + ], + 'slack' => [ + { + required: true, + name: :webhook, + type: String, + desc: 'The Slack webhook. e.g. https://hooks.slack.com/services/...' + }, + { + required: false, + name: :new_issue_url, + type: String, + desc: 'The user name' + }, + { + required: false, + name: :channel, + type: String, + desc: 'The channel name' + } + ], + 'mattermost' => [ + { + required: true, + name: :webhook, + type: String, + desc: 'The Mattermost webhook. e.g. http://mattermost_host/hooks/...' + } + ], + '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' + } + ] + } + + trigger_services = { + 'mattermost-slash-commands' => [ + { + name: :token, + type: String, + desc: 'The Mattermost token' + } + ], + 'slack-slash-commands' => [ + { + name: :token, + type: String, + desc: 'The Slack token' + } + ] + }.freeze + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects, requirements: { id: %r{[^/]+} } do + before { authenticate! } + before { authorize_admin_project } + + helpers do + def service_attributes(service) + service.fields.inject([]) do |arr, hash| + arr << hash[:name].to_sym + end + end + end + + desc "Delete a service for project" + params do + requires :service_slug, type: String, values: services.keys, desc: 'The name of the service' + end + delete ":id/services/:service_slug" do + service = user_project.find_or_initialize_service(params[:service_slug].underscore) + + attrs = service_attributes(service).inject({}) do |hash, key| + hash.merge!(key => nil) + end + + if service.update_attributes(attrs.merge(active: false)) + status(200) + true + else + render_api_error!('400 Bad Request', 400) + end + end + + desc 'Get the service settings for project' do + success Entities::ProjectService + end + params do + requires :service_slug, type: String, values: services.keys, desc: 'The name of the service' + end + get ":id/services/:service_slug" do + service = user_project.find_or_initialize_service(params[:service_slug].underscore) + present service, with: Entities::ProjectService, include_passwords: current_user.is_admin? + end + end + + trigger_services.each do |service_slug, settings| + helpers do + def chat_command_service(project, service_slug, params) + project.services.active.where(template: false).find do |service| + service.try(:token) == params[:token] && service.to_param == service_slug.underscore + end + end + end + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects, requirements: { id: %r{[^/]+} } do + desc "Trigger a slash command for #{service_slug}" do + detail 'Added in GitLab 8.13' + end + params do + settings.each do |setting| + requires setting[:name], type: setting[:type], desc: setting[:desc] + end + end + post ":id/services/#{service_slug.underscore}/trigger" do + project = find_project(params[:id]) + + # This is not accurate, but done to prevent leakage of the project names + not_found!('Service') unless project + + service = chat_command_service(project, service_slug, params) + result = service.try(:trigger, params) + + if result + status result[:status] || 200 + present result + else + not_found!('Service') + end + end + end + end + end + end +end diff --git a/lib/api/v3/settings.rb b/lib/api/v3/settings.rb new file mode 100644 index 00000000000..748d6b97d4f --- /dev/null +++ b/lib/api/v3/settings.rb @@ -0,0 +1,137 @@ +module API + module V3 + class Settings < Grape::API + before { authenticated_as_admin! } + + helpers do + def current_settings + @current_setting ||= + (ApplicationSetting.current || ApplicationSetting.create_from_defaults) + end + end + + desc 'Get the current application settings' do + success Entities::ApplicationSetting + end + get "application/settings" do + present current_settings, with: Entities::ApplicationSetting + end + + desc 'Modify application settings' do + success Entities::ApplicationSetting + end + params do + optional :default_branch_protection, type: Integer, values: [0, 1, 2], desc: 'Determine if developers can push to master' + optional :default_project_visibility, type: Integer, values: Gitlab::VisibilityLevel.values, desc: 'The default project visibility' + optional :default_snippet_visibility, type: Integer, values: Gitlab::VisibilityLevel.values, desc: 'The default snippet visibility' + optional :default_group_visibility, type: Integer, values: Gitlab::VisibilityLevel.values, desc: 'The default group visibility' + optional :restricted_visibility_levels, type: Array[String], desc: 'Selected levels cannot be used by non-admin users for projects or snippets. If the public level is restricted, user profiles are only visible to logged in users.' + optional :import_sources, type: Array[String], values: %w[github bitbucket gitlab google_code fogbugz git gitlab_project], + desc: 'Enabled sources for code import during project creation. OmniAuth must be configured for GitHub, Bitbucket, and GitLab.com' + optional :disabled_oauth_sign_in_sources, type: Array[String], desc: 'Disable certain OAuth sign-in sources' + optional :enabled_git_access_protocol, type: String, values: %w[ssh http nil], desc: 'Allow only the selected protocols to be used for Git access.' + optional :gravatar_enabled, type: Boolean, desc: 'Flag indicating if the Gravatar service is enabled' + optional :default_projects_limit, type: Integer, desc: 'The maximum number of personal projects' + optional :max_attachment_size, type: Integer, desc: 'Maximum attachment size in MB' + optional :session_expire_delay, type: Integer, desc: 'Session duration in minutes. GitLab restart is required to apply changes.' + optional :user_oauth_applications, type: Boolean, desc: 'Allow users to register any application to use GitLab as an OAuth provider' + optional :user_default_external, type: Boolean, desc: 'Newly registered users will by default be external' + optional :signup_enabled, type: Boolean, desc: 'Flag indicating if sign up is enabled' + optional :send_user_confirmation_email, type: Boolean, desc: 'Send confirmation email on sign-up' + optional :domain_whitelist, type: String, desc: 'ONLY users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com' + optional :domain_blacklist_enabled, type: Boolean, desc: 'Enable domain blacklist for sign ups' + given domain_blacklist_enabled: ->(val) { val } do + requires :domain_blacklist, type: String, desc: 'Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com' + end + optional :after_sign_up_text, type: String, desc: 'Text shown after sign up' + optional :signin_enabled, type: Boolean, desc: 'Flag indicating if sign in is enabled' + optional :require_two_factor_authentication, type: Boolean, desc: 'Require all users to setup Two-factor authentication' + given require_two_factor_authentication: ->(val) { val } do + requires :two_factor_grace_period, type: Integer, desc: 'Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication' + end + optional :home_page_url, type: String, desc: 'We will redirect non-logged in users to this page' + optional :after_sign_out_path, type: String, desc: 'We will redirect users to this page after they sign out' + optional :sign_in_text, type: String, desc: 'The sign in text of the GitLab application' + optional :help_page_text, type: String, desc: 'Custom text displayed on the help page' + optional :shared_runners_enabled, type: Boolean, desc: 'Enable shared runners for new projects' + given shared_runners_enabled: ->(val) { val } do + requires :shared_runners_text, type: String, desc: 'Shared runners text ' + end + optional :max_artifacts_size, type: Integer, desc: "Set the maximum file size each build's artifacts can have" + optional :max_pages_size, type: Integer, desc: 'Maximum size of pages in MB' + optional :container_registry_token_expire_delay, type: Integer, desc: 'Authorization token duration (minutes)' + optional :metrics_enabled, type: Boolean, desc: 'Enable the InfluxDB metrics' + given metrics_enabled: ->(val) { val } do + requires :metrics_host, type: String, desc: 'The InfluxDB host' + requires :metrics_port, type: Integer, desc: 'The UDP port to use for connecting to InfluxDB' + requires :metrics_pool_size, type: Integer, desc: 'The amount of InfluxDB connections to open' + requires :metrics_timeout, type: Integer, desc: 'The amount of seconds after which an InfluxDB connection will time out' + requires :metrics_method_call_threshold, type: Integer, desc: 'A method call is only tracked when it takes longer to complete than the given amount of milliseconds.' + requires :metrics_sample_interval, type: Integer, desc: 'The sampling interval in seconds' + requires :metrics_packet_size, type: Integer, desc: 'The amount of points to store in a single UDP packet' + end + optional :sidekiq_throttling_enabled, type: Boolean, desc: 'Enable Sidekiq Job Throttling' + given sidekiq_throttling_enabled: ->(val) { val } do + requires :sidekiq_throttling_queus, type: Array[String], desc: 'Choose which queues you wish to throttle' + requires :sidekiq_throttling_factor, type: Float, desc: 'The factor by which the queues should be throttled. A value between 0.0 and 1.0, exclusive.' + end + optional :recaptcha_enabled, type: Boolean, desc: 'Helps prevent bots from creating accounts' + given recaptcha_enabled: ->(val) { val } do + requires :recaptcha_site_key, type: String, desc: 'Generate site key at http://www.google.com/recaptcha' + requires :recaptcha_private_key, type: String, desc: 'Generate private key at http://www.google.com/recaptcha' + end + optional :akismet_enabled, type: Boolean, desc: 'Helps prevent bots from creating issues' + given akismet_enabled: ->(val) { val } do + requires :akismet_api_key, type: String, desc: 'Generate API key at http://www.akismet.com' + end + optional :admin_notification_email, type: String, desc: 'Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area.' + optional :sentry_enabled, type: Boolean, desc: 'Sentry is an error reporting and logging tool which is currently not shipped with GitLab, get it here: https://getsentry.com' + given sentry_enabled: ->(val) { val } do + requires :sentry_dsn, type: String, desc: 'Sentry Data Source Name' + end + optional :repository_storage, type: String, desc: 'Storage paths for new projects' + optional :repository_checks_enabled, type: Boolean, desc: "GitLab will periodically run 'git fsck' in all project and wiki repositories to look for silent disk corruption issues." + optional :koding_enabled, type: Boolean, desc: 'Enable Koding' + given koding_enabled: ->(val) { val } do + requires :koding_url, type: String, desc: 'The Koding team URL' + end + optional :plantuml_enabled, type: Boolean, desc: 'Enable PlantUML' + given plantuml_enabled: ->(val) { val } do + requires :plantuml_url, type: String, desc: 'The PlantUML server URL' + end + optional :version_check_enabled, type: Boolean, desc: 'Let GitLab inform you when an update is available.' + optional :email_author_in_body, type: Boolean, desc: 'Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead.' + optional :html_emails_enabled, type: Boolean, desc: 'By default GitLab sends emails in HTML and plain text formats so mail clients can choose what format to use. Disable this option if you only want to send emails in plain text format.' + optional :housekeeping_enabled, type: Boolean, desc: 'Enable automatic repository housekeeping (git repack, git gc)' + given housekeeping_enabled: ->(val) { val } do + requires :housekeeping_bitmaps_enabled, type: Boolean, desc: "Creating pack file bitmaps makes housekeeping take a little longer but bitmaps should accelerate 'git clone' performance." + requires :housekeeping_incremental_repack_period, type: Integer, desc: "Number of Git pushes after which an incremental 'git repack' is run." + requires :housekeeping_full_repack_period, type: Integer, desc: "Number of Git pushes after which a full 'git repack' is run." + requires :housekeeping_gc_period, type: Integer, desc: "Number of Git pushes after which 'git gc' is run." + end + optional :terminal_max_session_time, type: Integer, desc: 'Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time.' + at_least_one_of :default_branch_protection, :default_project_visibility, :default_snippet_visibility, + :default_group_visibility, :restricted_visibility_levels, :import_sources, + :enabled_git_access_protocol, :gravatar_enabled, :default_projects_limit, + :max_attachment_size, :session_expire_delay, :disabled_oauth_sign_in_sources, + :user_oauth_applications, :user_default_external, :signup_enabled, + :send_user_confirmation_email, :domain_whitelist, :domain_blacklist_enabled, + :after_sign_up_text, :signin_enabled, :require_two_factor_authentication, + :home_page_url, :after_sign_out_path, :sign_in_text, :help_page_text, + :shared_runners_enabled, :max_artifacts_size, :max_pages_size, :container_registry_token_expire_delay, + :metrics_enabled, :sidekiq_throttling_enabled, :recaptcha_enabled, + :akismet_enabled, :admin_notification_email, :sentry_enabled, + :repository_storage, :repository_checks_enabled, :koding_enabled, :plantuml_enabled, + :version_check_enabled, :email_author_in_body, :html_emails_enabled, + :housekeeping_enabled, :terminal_max_session_time + end + put "application/settings" do + if current_settings.update_attributes(declared_params(include_missing: false)) + present current_settings, with: Entities::ApplicationSetting + else + render_validation_error!(current_settings) + end + end + end + end +end diff --git a/lib/api/v3/snippets.rb b/lib/api/v3/snippets.rb new file mode 100644 index 00000000000..07dac7e9904 --- /dev/null +++ b/lib/api/v3/snippets.rb @@ -0,0 +1,138 @@ +module API + module V3 + class Snippets < Grape::API + include PaginationParams + + before { authenticate! } + + resource :snippets do + helpers do + def snippets_for_current_user + SnippetsFinder.new.execute(current_user, filter: :by_user, user: current_user) + end + + def public_snippets + SnippetsFinder.new.execute(current_user, filter: :public) + end + end + + desc 'Get a snippets list for authenticated user' do + detail 'This feature was introduced in GitLab 8.15.' + success ::API::Entities::PersonalSnippet + end + params do + use :pagination + end + get do + present paginate(snippets_for_current_user), with: ::API::Entities::PersonalSnippet + end + + desc 'List all public snippets current_user has access to' do + detail 'This feature was introduced in GitLab 8.15.' + success ::API::Entities::PersonalSnippet + end + params do + use :pagination + end + get 'public' do + present paginate(public_snippets), with: ::API::Entities::PersonalSnippet + end + + desc 'Get a single snippet' do + detail 'This feature was introduced in GitLab 8.15.' + success ::API::Entities::PersonalSnippet + end + params do + requires :id, type: Integer, desc: 'The ID of a snippet' + end + get ':id' do + snippet = snippets_for_current_user.find(params[:id]) + present snippet, with: ::API::Entities::PersonalSnippet + end + + desc 'Create new snippet' do + detail 'This feature was introduced in GitLab 8.15.' + success ::API::Entities::PersonalSnippet + end + params do + requires :title, type: String, desc: 'The title of a snippet' + requires :file_name, type: String, desc: 'The name of a snippet file' + requires :content, type: String, desc: 'The content of a snippet' + optional :visibility_level, type: Integer, + values: Gitlab::VisibilityLevel.values, + default: Gitlab::VisibilityLevel::INTERNAL, + desc: 'The visibility level of the snippet' + end + post do + attrs = declared_params(include_missing: false).merge(request: request, api: true) + snippet = CreateSnippetService.new(nil, current_user, attrs).execute + + if snippet.persisted? + present snippet, with: ::API::Entities::PersonalSnippet + else + render_validation_error!(snippet) + end + end + + desc 'Update an existing snippet' do + detail 'This feature was introduced in GitLab 8.15.' + success ::API::Entities::PersonalSnippet + end + params do + requires :id, type: Integer, desc: 'The ID of a snippet' + optional :title, type: String, desc: 'The title of a snippet' + optional :file_name, type: String, desc: 'The name of a snippet file' + optional :content, type: String, desc: 'The content of a snippet' + optional :visibility_level, type: Integer, + values: Gitlab::VisibilityLevel.values, + desc: 'The visibility level of the snippet' + at_least_one_of :title, :file_name, :content, :visibility_level + end + put ':id' do + snippet = snippets_for_current_user.find_by(id: params.delete(:id)) + return not_found!('Snippet') unless snippet + authorize! :update_personal_snippet, snippet + + attrs = declared_params(include_missing: false) + + UpdateSnippetService.new(nil, current_user, snippet, attrs).execute + if snippet.persisted? + present snippet, with: ::API::Entities::PersonalSnippet + else + render_validation_error!(snippet) + end + end + + desc 'Remove snippet' do + detail 'This feature was introduced in GitLab 8.15.' + success ::API::Entities::PersonalSnippet + end + params do + requires :id, type: Integer, desc: 'The ID of a snippet' + end + delete ':id' do + snippet = snippets_for_current_user.find_by(id: params.delete(:id)) + return not_found!('Snippet') unless snippet + authorize! :destroy_personal_snippet, snippet + snippet.destroy + no_content! + end + + desc 'Get a raw snippet' do + detail 'This feature was introduced in GitLab 8.15.' + end + params do + requires :id, type: Integer, desc: 'The ID of a snippet' + end + get ":id/raw" do + snippet = snippets_for_current_user.find_by(id: params.delete(:id)) + return not_found!('Snippet') unless snippet + + env['api.format'] = :txt + content_type 'text/plain' + present snippet.content + end + end + end + end +end diff --git a/lib/api/v3/subscriptions.rb b/lib/api/v3/subscriptions.rb index 02a4157c26e..068750ec077 100644 --- a/lib/api/v3/subscriptions.rb +++ b/lib/api/v3/subscriptions.rb @@ -14,7 +14,7 @@ module API requires :id, type: String, desc: 'The ID of a project' requires :subscribable_id, type: String, desc: 'The ID of a resource' end - resource :projects do + resource :projects, requirements: { id: %r{[^/]+} } do subscribable_types.each do |type, finder| type_singularized = type.singularize entity_class = ::API::Entities.const_get(type_singularized.camelcase) diff --git a/lib/api/v3/system_hooks.rb b/lib/api/v3/system_hooks.rb index 391510b9ee0..5787c06fc12 100644 --- a/lib/api/v3/system_hooks.rb +++ b/lib/api/v3/system_hooks.rb @@ -13,6 +13,19 @@ module API get do present SystemHook.all, with: ::API::Entities::Hook end + + desc 'Delete a hook' do + success ::API::Entities::Hook + end + params do + requires :id, type: Integer, desc: 'The ID of the system hook' + end + delete ":id" do + hook = SystemHook.find_by(id: params[:id]) + not_found!('System hook') unless hook + + present hook.destroy, with: ::API::Entities::Hook + end end end end diff --git a/lib/api/v3/tags.rb b/lib/api/v3/tags.rb index 016e3d86932..c2541de2f50 100644 --- a/lib/api/v3/tags.rb +++ b/lib/api/v3/tags.rb @@ -6,7 +6,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects do + resource :projects, requirements: { id: %r{[^/]+} } do desc 'Get a project repository tags' do success ::API::Entities::RepoTag end @@ -14,6 +14,26 @@ module API tags = user_project.repository.tags.sort_by(&:name).reverse present tags, with: ::API::Entities::RepoTag, project: user_project end + + desc 'Delete a repository tag' + params do + requires :tag_name, type: String, desc: 'The name of the tag' + end + delete ":id/repository/tags/:tag_name", requirements: { tag_name: /.+/ } do + authorize_push_project + + result = ::Tags::DestroyService.new(user_project, current_user). + execute(params[:tag_name]) + + if result[:status] == :success + status(200) + { + tag_name: params[:tag_name] + } + else + render_api_error!(result[:message], result[:return_code]) + end + end end end end diff --git a/lib/api/v3/time_tracking_endpoints.rb b/lib/api/v3/time_tracking_endpoints.rb new file mode 100644 index 00000000000..81ae4e8137d --- /dev/null +++ b/lib/api/v3/time_tracking_endpoints.rb @@ -0,0 +1,116 @@ +module API + module V3 + module TimeTrackingEndpoints + extend ActiveSupport::Concern + + included do + helpers do + def issuable_name + declared_params.has_key?(:issue_id) ? 'issue' : 'merge_request' + end + + def issuable_key + "#{issuable_name}_id".to_sym + end + + def update_issuable_key + "update_#{issuable_name}".to_sym + end + + def read_issuable_key + "read_#{issuable_name}".to_sym + end + + def load_issuable + @issuable ||= begin + case issuable_name + when 'issue' + find_project_issue(params.delete(issuable_key)) + when 'merge_request' + find_project_merge_request(params.delete(issuable_key)) + end + end + end + + def update_issuable(attrs) + custom_params = declared_params(include_missing: false) + custom_params.merge!(attrs) + + issuable = update_service.new(user_project, current_user, custom_params).execute(load_issuable) + if issuable.valid? + present issuable, with: ::API::Entities::IssuableTimeStats + else + render_validation_error!(issuable) + end + end + + def update_service + issuable_name == 'issue' ? ::Issues::UpdateService : ::MergeRequests::UpdateService + end + end + + issuable_name = name.end_with?('Issues') ? 'issue' : 'merge_request' + issuable_collection_name = issuable_name.pluralize + issuable_key = "#{issuable_name}_id".to_sym + + desc "Set a time estimate for a project #{issuable_name}" + params do + requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}" + requires :duration, type: String, desc: 'The duration to be parsed' + end + post ":id/#{issuable_collection_name}/:#{issuable_key}/time_estimate" do + authorize! update_issuable_key, load_issuable + + status :ok + update_issuable(time_estimate: Gitlab::TimeTrackingFormatter.parse(params.delete(:duration))) + end + + desc "Reset the time estimate for a project #{issuable_name}" + params do + requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}" + end + post ":id/#{issuable_collection_name}/:#{issuable_key}/reset_time_estimate" do + authorize! update_issuable_key, load_issuable + + status :ok + update_issuable(time_estimate: 0) + end + + desc "Add spent time for a project #{issuable_name}" + params do + requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}" + requires :duration, type: String, desc: 'The duration to be parsed' + end + post ":id/#{issuable_collection_name}/:#{issuable_key}/add_spent_time" do + authorize! update_issuable_key, load_issuable + + update_issuable(spend_time: { + duration: Gitlab::TimeTrackingFormatter.parse(params.delete(:duration)), + user: current_user + }) + end + + desc "Reset spent time for a project #{issuable_name}" + params do + requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}" + end + post ":id/#{issuable_collection_name}/:#{issuable_key}/reset_spent_time" do + authorize! update_issuable_key, load_issuable + + status :ok + update_issuable(spend_time: { duration: :reset, user: current_user }) + end + + desc "Show time stats for a project #{issuable_name}" + params do + requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}" + end + get ":id/#{issuable_collection_name}/:#{issuable_key}/time_stats" do + authorize! read_issuable_key, load_issuable + + present load_issuable, with: ::API::Entities::IssuableTimeStats + end + end + end + end +end diff --git a/lib/api/v3/todos.rb b/lib/api/v3/todos.rb index 4f9b5fe72a6..e3b311d61cd 100644 --- a/lib/api/v3/todos.rb +++ b/lib/api/v3/todos.rb @@ -19,8 +19,10 @@ module API desc 'Mark all todos as done' delete do + status(200) + todos = TodosFinder.new(current_user, params).execute - TodoService.new.mark_todos_as_done(todos, current_user) + TodoService.new.mark_todos_as_done(todos, current_user).size end end end diff --git a/lib/api/v3/triggers.rb b/lib/api/v3/triggers.rb new file mode 100644 index 00000000000..a23d6b6b48c --- /dev/null +++ b/lib/api/v3/triggers.rb @@ -0,0 +1,103 @@ +module API + module V3 + class Triggers < Grape::API + include PaginationParams + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects, requirements: { id: %r{[^/]+} } do + desc 'Trigger a GitLab project build' do + success ::API::V3::Entities::TriggerRequest + end + params do + requires :ref, type: String, desc: 'The commit sha or name of a branch or tag' + requires :token, type: String, desc: 'The unique token of trigger' + optional :variables, type: Hash, desc: 'The list of variables to be injected into build' + end + post ":id/(ref/:ref/)trigger/builds", requirements: { ref: /.+/ } do + project = find_project(params[:id]) + trigger = Ci::Trigger.find_by_token(params[:token].to_s) + not_found! unless project && trigger + unauthorized! unless trigger.project == project + + # validate variables + variables = params[:variables].to_h + unless variables.all? { |key, value| key.is_a?(String) && value.is_a?(String) } + render_api_error!('variables needs to be a map of key-valued strings', 400) + end + + # create request and trigger builds + trigger_request = Ci::CreateTriggerRequestService.new.execute(project, trigger, params[:ref].to_s, variables) + if trigger_request + present trigger_request, with: ::API::V3::Entities::TriggerRequest + else + errors = 'No builds created' + render_api_error!(errors, 400) + end + end + + desc 'Get triggers list' do + success ::API::V3::Entities::Trigger + end + params do + use :pagination + end + get ':id/triggers' do + authenticate! + authorize! :admin_build, user_project + + triggers = user_project.triggers.includes(:trigger_requests) + + present paginate(triggers), with: ::API::V3::Entities::Trigger + end + + desc 'Get specific trigger of a project' do + success ::API::V3::Entities::Trigger + end + params do + requires :token, type: String, desc: 'The unique token of trigger' + end + get ':id/triggers/:token' do + authenticate! + authorize! :admin_build, user_project + + trigger = user_project.triggers.find_by(token: params[:token].to_s) + return not_found!('Trigger') unless trigger + + present trigger, with: ::API::V3::Entities::Trigger + end + + desc 'Create a trigger' do + success ::API::V3::Entities::Trigger + end + post ':id/triggers' do + authenticate! + authorize! :admin_build, user_project + + trigger = user_project.triggers.create + + present trigger, with: ::API::V3::Entities::Trigger + end + + desc 'Delete a trigger' do + success ::API::V3::Entities::Trigger + end + params do + requires :token, type: String, desc: 'The unique token of trigger' + end + delete ':id/triggers/:token' do + authenticate! + authorize! :admin_build, user_project + + trigger = user_project.triggers.find_by(token: params[:token].to_s) + return not_found!('Trigger') unless trigger + + trigger.destroy + + present trigger, with: ::API::V3::Entities::Trigger + end + end + end + end +end diff --git a/lib/api/v3/users.rb b/lib/api/v3/users.rb index e05e457a5df..14f54731730 100644 --- a/lib/api/v3/users.rb +++ b/lib/api/v3/users.rb @@ -71,6 +71,46 @@ module API user.activate end end + + desc 'Get the contribution events of a specified user' do + detail 'This feature was introduced in GitLab 8.13.' + success ::API::V3::Entities::Event + end + params do + requires :id, type: Integer, desc: 'The ID of the user' + use :pagination + end + get ':id/events' do + user = User.find_by(id: params[:id]) + not_found!('User') unless user + + events = user.events. + merge(ProjectsFinder.new.execute(current_user)). + references(:project). + with_associations. + recent + + present paginate(events), with: ::API::V3::Entities::Event + end + + desc 'Delete an existing SSH key from a specified user. Available only for admins.' do + success ::API::Entities::SSHKey + end + params do + requires :id, type: Integer, desc: 'The ID of the user' + requires :key_id, type: Integer, desc: 'The ID of the SSH key' + end + delete ':id/keys/:key_id' do + authenticated_as_admin! + + user = User.find_by(id: params[:id]) + not_found!('User') unless user + + key = user.keys.find_by(id: params[:key_id]) + not_found!('Key') unless key + + present key.destroy, with: ::API::Entities::SSHKey + end end resource :user do @@ -90,6 +130,19 @@ module API get "emails" do present current_user.emails, with: ::API::Entities::Email end + + desc 'Delete an SSH key from the currently authenticated user' do + success ::API::Entities::SSHKey + end + params do + requires :key_id, type: Integer, desc: 'The ID of the SSH key' + end + delete "keys/:key_id" do + key = current_user.keys.find_by(id: params[:key_id]) + not_found!('Key') unless key + + present key.destroy, with: ::API::Entities::SSHKey + end end end end diff --git a/lib/api/v3/variables.rb b/lib/api/v3/variables.rb new file mode 100644 index 00000000000..83972b1e7ce --- /dev/null +++ b/lib/api/v3/variables.rb @@ -0,0 +1,29 @@ +module API + module V3 + class Variables < Grape::API + include PaginationParams + + before { authenticate! } + before { authorize! :admin_build, user_project } + + params do + requires :id, type: String, desc: 'The ID of a project' + end + + resource :projects, requirements: { id: %r{[^/]+} } do + desc 'Delete an existing variable from a project' do + success ::API::Entities::Variable + end + params do + requires :key, type: String, desc: 'The key of the variable' + end + delete ':id/variables/:key' do + variable = user_project.variables.find_by(key: params[:key]) + not_found!('Variable') unless variable + + present variable.destroy, with: ::API::Entities::Variable + end + end + end + end +end diff --git a/lib/api/variables.rb b/lib/api/variables.rb index f623b1dfe9f..5acde41551b 100644 --- a/lib/api/variables.rb +++ b/lib/api/variables.rb @@ -1,5 +1,4 @@ module API - # Projects variables API class Variables < Grape::API include PaginationParams @@ -10,7 +9,7 @@ module API requires :id, type: String, desc: 'The ID of a project' end - resource :projects do + resource :projects, requirements: { id: %r{[^/]+} } do desc 'Get project variables' do success Entities::Variable end @@ -81,10 +80,9 @@ module API end delete ':id/variables/:key' do variable = user_project.variables.find_by(key: params[:key]) + not_found!('Variable') unless variable - return not_found!('Variable') unless variable - - present variable.destroy, with: Entities::Variable + variable.destroy end end end |