diff options
author | Douwe Maan <douwe@selenight.nl> | 2017-07-31 10:23:50 +0200 |
---|---|---|
committer | Douwe Maan <douwe@selenight.nl> | 2017-07-31 10:23:50 +0200 |
commit | 95f9d6d83bcb7a9f45601f828c34301e536fc195 (patch) | |
tree | ed5447c85eb35ac9b7a363dd5a83514578c2fb3e /lib | |
parent | c92881c394a482bdb6d3f0f0a5f3cc95d5eed1fc (diff) | |
parent | 6fe736f2c927340ed8cf827080db781f7c8adcdf (diff) | |
download | gitlab-ce-95f9d6d83bcb7a9f45601f828c34301e536fc195.tar.gz |
Merge branch 'master' into issue-discussions-refactor
# Conflicts:
# app/controllers/concerns/notes_actions.rb
# spec/features/task_lists_spec.rb
# spec/support/features/issuable_slash_commands_shared_examples.rb
Diffstat (limited to 'lib')
62 files changed, 1131 insertions, 702 deletions
diff --git a/lib/api/api.rb b/lib/api/api.rb index efcf0976a81..045a0db1842 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -3,6 +3,7 @@ module API include APIGuard allow_access_with_scope :api + prefix :api version %w(v3 v4), using: :path @@ -85,6 +86,9 @@ module API helpers ::API::Helpers helpers ::API::Helpers::CommonHelpers + NO_SLASH_URL_PART_REGEX = %r{[^/]+} + PROJECT_ENDPOINT_REQUIREMENTS = { id: NO_SLASH_URL_PART_REGEX }.freeze + # Keep in alphabetical order mount ::API::AccessRequests mount ::API::AwardEmoji @@ -109,7 +113,8 @@ module API mount ::API::Members mount ::API::MergeRequestDiffs mount ::API::MergeRequests - mount ::API::Milestones + mount ::API::ProjectMilestones + mount ::API::GroupMilestones mount ::API::Namespaces mount ::API::Notes mount ::API::NotificationSettings diff --git a/lib/api/branches.rb b/lib/api/branches.rb index 3d816f8771d..9dd60d1833b 100644 --- a/lib/api/branches.rb +++ b/lib/api/branches.rb @@ -4,19 +4,21 @@ module API class Branches < Grape::API include PaginationParams + BRANCH_ENDPOINT_REQUIREMENTS = API::PROJECT_ENDPOINT_REQUIREMENTS.merge(branch: API::NO_SLASH_URL_PART_REGEX) + before { authorize! :download_code, user_project } params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects, requirements: { id: %r{[^/]+} } do + resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do desc 'Get a project repository branches' do success Entities::RepoBranch end params do use :pagination end - get ":id/repository/branches" do + get ':id/repository/branches' do branches = ::Kaminari.paginate_array(user_project.repository.branches.sort_by(&:name)) present paginate(branches), with: Entities::RepoBranch, project: user_project @@ -28,7 +30,7 @@ module API params do requires :branch, type: String, desc: 'The name of the branch' end - get ':id/repository/branches/:branch', requirements: { branch: /.+/ } do + get ':id/repository/branches/:branch', requirements: BRANCH_ENDPOINT_REQUIREMENTS do branch = user_project.repository.find_branch(params[:branch]) not_found!("Branch") unless branch @@ -46,7 +48,7 @@ module API optional :developers_can_push, type: Boolean, desc: 'Flag if developers can push to that branch' optional :developers_can_merge, type: Boolean, desc: 'Flag if developers can merge to that branch' end - put ':id/repository/branches/:branch/protect', requirements: { branch: /.+/ } do + put ':id/repository/branches/:branch/protect', requirements: BRANCH_ENDPOINT_REQUIREMENTS do authorize_admin_project branch = user_project.repository.find_branch(params[:branch]) @@ -81,7 +83,7 @@ module API params do requires :branch, type: String, desc: 'The name of the branch' end - put ':id/repository/branches/:branch/unprotect', requirements: { branch: /.+/ } do + put ':id/repository/branches/:branch/unprotect', requirements: BRANCH_ENDPOINT_REQUIREMENTS do authorize_admin_project branch = user_project.repository.find_branch(params[:branch]) @@ -99,7 +101,7 @@ module API requires :branch, 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 + post ':id/repository/branches' do authorize_push_project result = CreateBranchService.new(user_project, current_user) @@ -118,7 +120,7 @@ module API params do requires :branch, type: String, desc: 'The name of the branch' end - delete ":id/repository/branches/:branch", requirements: { branch: /.+/ } do + delete ':id/repository/branches/:branch', requirements: BRANCH_ENDPOINT_REQUIREMENTS do authorize_push_project result = DeleteBranchService.new(user_project, current_user) @@ -130,7 +132,7 @@ module API end desc 'Delete all merged branches' - delete ":id/repository/merged_branches" do + delete ':id/repository/merged_branches' do DeleteMergedBranchesService.new(user_project, current_user).async_execute accepted! diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 89ab2118dd5..0e035479101 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -82,6 +82,38 @@ module API end class Project < Grape::Entity + include ::API::Helpers::RelatedResourcesHelpers + + expose :_links do + expose :self do |project| + expose_url(api_v4_projects_path(id: project.id)) + end + + expose :issues, if: -> (*args) { issues_available?(*args) } do |project| + expose_url(api_v4_projects_issues_path(id: project.id)) + end + + expose :merge_requests, if: -> (*args) { mrs_available?(*args) } do |project| + expose_url(api_v4_projects_merge_requests_path(id: project.id)) + end + + expose :repo_branches do |project| + expose_url(api_v4_projects_repository_branches_path(id: project.id)) + end + + expose :labels do |project| + expose_url(api_v4_projects_labels_path(id: project.id)) + end + + expose :events do |project| + expose_url(api_v4_projects_events_path(id: project.id)) + end + + expose :members do |project| + expose_url(api_v4_projects_members_path(id: project.id)) + end + end + expose :id, :description, :default_branch, :tag_list expose :archived?, as: :archived expose :visibility, :ssh_url_to_repo, :http_url_to_repo, :web_url @@ -109,7 +141,7 @@ module API user.avatar_url(only_path: false) end 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 :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[:current_user]) } expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] } expose :public_builds, as: :public_jobs expose :ci_config_path @@ -269,8 +301,8 @@ module API class Milestone < Grape::Entity expose :id, :iid - expose(:project_id) { |entity| entity&.project_id } - expose(:group_id) { |entity| entity&.group_id } + expose :project_id, if: -> (entity, options) { entity&.project_id } + expose :group_id, if: -> (entity, options) { entity&.group_id } expose :title, :description expose :state, :created_at, :updated_at expose :due_date @@ -297,6 +329,26 @@ module API end class Issue < IssueBasic + include ::API::Helpers::RelatedResourcesHelpers + + expose :_links do + expose :self do |issue| + expose_url(api_v4_project_issue_path(id: issue.project_id, issue_iid: issue.iid)) + end + + expose :notes do |issue| + expose_url(api_v4_projects_issues_notes_path(id: issue.project_id, noteable_id: issue.iid)) + end + + expose :award_emoji do |issue| + expose_url(api_v4_projects_issues_award_emoji_path(id: issue.project_id, issue_iid: issue.iid)) + end + + expose :project do |issue| + expose_url(api_v4_projects_path(id: issue.project_id)) + end + end + expose :subscribed do |issue, options| issue.subscribed?(options[:current_user], options[:project] || issue.project) end @@ -619,43 +671,14 @@ module API class ApplicationSetting < Grape::Entity expose :id - expose :default_projects_limit - expose :signup_enabled - expose :password_authentication_enabled - expose :password_authentication_enabled, as: :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(*::ApplicationSettingsHelper.visible_attributes) 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) { |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 - 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 - expose :polling_interval_multiplier - expose :help_page_hide_commercial_content - expose :help_page_text - expose :help_page_support_url + expose :password_authentication_enabled, as: :signin_enabled end class Release < Grape::Entity diff --git a/lib/api/group_milestones.rb b/lib/api/group_milestones.rb new file mode 100644 index 00000000000..b85eb59dc0a --- /dev/null +++ b/lib/api/group_milestones.rb @@ -0,0 +1,85 @@ +module API + class GroupMilestones < Grape::API + include MilestoneResponses + include PaginationParams + + before do + authenticate! + end + + params do + requires :id, type: String, desc: 'The ID of a group' + end + resource :groups, requirements: { id: %r{[^/]+} } do + desc 'Get a list of group milestones' do + success Entities::Milestone + end + params do + use :list_params + end + get ":id/milestones" do + list_milestones_for(user_group) + end + + desc 'Get a single group milestone' do + success Entities::Milestone + end + params do + requires :milestone_id, type: Integer, desc: 'The ID of a group milestone' + end + get ":id/milestones/:milestone_id" do + authorize! :read_group, user_group + + get_milestone_for(user_group) + end + + desc 'Create a new group milestone' do + success Entities::Milestone + end + params do + requires :title, type: String, desc: 'The title of the milestone' + use :optional_params + end + post ":id/milestones" do + authorize! :admin_milestones, user_group + + create_milestone_for(user_group) + end + + desc 'Update an existing group milestone' do + success Entities::Milestone + end + params do + use :update_params + end + put ":id/milestones/:milestone_id" do + authorize! :admin_milestones, user_group + + update_milestone_for(user_group) + end + + desc 'Get all issues for a single group milestone' do + success Entities::IssueBasic + end + params do + requires :milestone_id, type: Integer, desc: 'The ID of a group milestone' + use :pagination + end + get ":id/milestones/:milestone_id/issues" do + milestone_issuables_for(user_group, :issue) + end + + desc 'Get all merge requests for a single group milestone' do + detail 'This feature was introduced in GitLab 9.' + success Entities::MergeRequestBasic + end + params do + requires :milestone_id, type: Integer, desc: 'The ID of a group milestone' + use :pagination + end + get ':id/milestones/:milestone_id/merge_requests' do + milestone_issuables_for(user_group, :merge_request) + end + end + end +end diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 0f4791841d2..234825480f2 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -25,6 +25,10 @@ module API initial_current_user != current_user end + def user_group + @group ||= find_group!(params[:id]) + end + def user_project @project ||= find_project!(params[:id]) end @@ -332,12 +336,14 @@ module API env['warden'] end + # Check if the request is GET/HEAD, or if CSRF token is valid. + def verified_request? + Gitlab::RequestForgeryProtection.verified?(env) + end + # Check the Rails session for valid authentication details - # - # Until CSRF protection is added to the API, disallow this method for - # state-changing endpoints def find_user_from_warden - warden.try(:authenticate) if %w[GET HEAD].include?(env['REQUEST_METHOD']) + warden.try(:authenticate) if verified_request? end def initial_current_user diff --git a/lib/api/helpers/related_resources_helpers.rb b/lib/api/helpers/related_resources_helpers.rb new file mode 100644 index 00000000000..1f677529b07 --- /dev/null +++ b/lib/api/helpers/related_resources_helpers.rb @@ -0,0 +1,28 @@ +module API + module Helpers + module RelatedResourcesHelpers + include GrapeRouteHelpers::NamedRouteMatcher + + def issues_available?(project, options) + available?(:issues, project, options[:current_user]) + end + + def mrs_available?(project, options) + available?(:merge_requests, project, options[:current_user]) + end + + def expose_url(path) + url_options = Gitlab::Application.routes.default_url_options + protocol, host, port = url_options.slice(:protocol, :host, :port).values + + URI::HTTP.build(scheme: protocol, host: host, port: port, path: path).to_s + end + + private + + def available?(feature, project, current_user) + project.feature_available?(feature, current_user) + end + end + end +end diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 14b26f28ebf..4cec1145f3a 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -29,6 +29,10 @@ module API optional :search, type: String, desc: 'Search issues for text present in the title or description' optional :created_after, type: DateTime, desc: 'Return issues created after the specified time' optional :created_before, type: DateTime, desc: 'Return issues created before the specified time' + optional :author_id, type: Integer, desc: 'Return issues which are authored by the user with the given ID' + optional :assignee_id, type: Integer, desc: 'Return issues which are assigned to the user with the given ID' + optional :scope, type: String, values: %w[created-by-me assigned-to-me all], + desc: 'Return issues for the given scope: `created-by-me`, `assigned-to-me` or `all`' use :pagination end @@ -55,9 +59,11 @@ module API optional :state, type: String, values: %w[opened closed all], default: 'all', desc: 'Return opened, closed, or all issues' use :issues_params + optional :scope, type: String, values: %w[created-by-me assigned-to-me all], default: 'created-by-me', + desc: 'Return issues for the given scope: `created-by-me`, `assigned-to-me` or `all`' end get do - issues = find_issues(scope: 'authored') + issues = find_issues present paginate(issues), with: Entities::IssueBasic, current_user: current_user end @@ -112,7 +118,7 @@ module API params do requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue' end - get ":id/issues/:issue_iid" do + get ":id/issues/:issue_iid", as: :api_v4_project_issue do issue = find_project_issue(params[:issue_iid]) present issue, with: Entities::Issue, current_user: current_user, project: user_project end diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 6e2e13e0a24..8810d4e441d 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -4,14 +4,77 @@ module API before { authenticate! } + helpers ::Gitlab::IssuableMetadata + + helpers do + def find_merge_requests(args = {}) + args = params.merge(args) + + args[:milestone_title] = args.delete(:milestone) + args[:label_name] = args.delete(:labels) + + merge_requests = MergeRequestsFinder.new(current_user, args).execute + .reorder(args[:order_by] => args[:sort]) + merge_requests = paginate(merge_requests) + .preload(:target_project) + + return merge_requests if args[:view] == 'simple' + + merge_requests + .preload(:notes, :author, :assignee, :milestone, :merge_request_diff, :labels) + end + + params :merge_requests_params do + optional :state, type: String, values: %w[opened closed merged all], default: 'all', + desc: 'Return opened, closed, merged, or all merge requests' + optional :order_by, type: String, values: %w[created_at updated_at], default: 'created_at', + desc: 'Return merge requests ordered by `created_at` or `updated_at` fields.' + optional :sort, type: String, values: %w[asc desc], default: 'desc', + desc: 'Return merge requests sorted in `asc` or `desc` order.' + optional :milestone, type: String, desc: 'Return merge requests for a specific milestone' + optional :labels, type: String, desc: 'Comma-separated list of label names' + optional :created_after, type: DateTime, desc: 'Return merge requests created after the specified time' + optional :created_before, type: DateTime, desc: 'Return merge requests created before the specified time' + optional :view, type: String, values: %w[simple], desc: 'If simple, returns the `iid`, URL, title, description, and basic state of merge request' + optional :author_id, type: Integer, desc: 'Return merge requests which are authored by the user with the given ID' + optional :assignee_id, type: Integer, desc: 'Return merge requests which are assigned to the user with the given ID' + optional :scope, type: String, values: %w[created-by-me assigned-to-me all], + desc: 'Return merge requests for the given scope: `created-by-me`, `assigned-to-me` or `all`' + use :pagination + end + end + + resource :merge_requests do + desc 'List merge requests' do + success Entities::MergeRequestBasic + end + params do + use :merge_requests_params + optional :scope, type: String, values: %w[created-by-me assigned-to-me all], default: 'created-by-me', + desc: 'Return merge requests for the given scope: `created-by-me`, `assigned-to-me` or `all`' + end + get do + merge_requests = find_merge_requests + + options = { with: Entities::MergeRequestBasic, + current_user: current_user } + + if params[:view] == 'simple' + options[:with] = Entities::MergeRequestSimple + else + options[:issuable_metadata] = issuable_meta_data(merge_requests, 'MergeRequest') + end + + present merge_requests, options + end + end + params do requires :id, type: String, desc: 'The ID of a project' end resource :projects, requirements: { id: %r{[^/]+} } do include TimeTrackingEndpoints - helpers ::Gitlab::IssuableMetadata - helpers do def handle_merge_request_errors!(errors) if errors[:project_access].any? @@ -29,31 +92,6 @@ 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 - - def find_merge_requests(args = {}) - args = params.merge(args) - - args[:milestone_title] = args.delete(:milestone) - args[:label_name] = args.delete(:labels) - - merge_requests = MergeRequestsFinder.new(current_user, args).execute - .reorder(args[:order_by] => args[:sort]) - merge_requests = paginate(merge_requests) - .preload(:target_project) - - return merge_requests if args[:view] == 'simple' - - merge_requests - .preload(:notes, :author, :assignee, :milestone, :merge_request_diff, :labels) - end - params :optional_params_ce 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' @@ -71,19 +109,8 @@ module API success Entities::MergeRequestBasic end params do - optional :state, type: String, values: %w[opened closed merged all], default: 'all', - desc: 'Return opened, closed, merged, or all merge requests' - optional :order_by, type: String, values: %w[created_at updated_at], default: 'created_at', - desc: 'Return merge requests ordered by `created_at` or `updated_at` fields.' - optional :sort, type: String, values: %w[asc desc], default: 'desc', - desc: 'Return merge requests sorted in `asc` or `desc` order.' + use :merge_requests_params optional :iids, type: Array[Integer], desc: 'The IID array of merge requests' - optional :milestone, type: String, desc: 'Return merge requests for a specific milestone' - optional :labels, type: String, desc: 'Comma-separated list of label names' - optional :created_after, type: DateTime, desc: 'Return merge requests created after the specified time' - optional :created_before, type: DateTime, desc: 'Return merge requests created before the specified time' - optional :view, type: String, values: %w[simple], desc: 'If simple, returns the `iid`, URL, title, description, and basic state of merge request' - use :pagination end get ":id/merge_requests" do authorize! :read_merge_request, user_project @@ -278,7 +305,14 @@ module API 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 + issues = paginate(issues) + + external_issues, internal_issues = issues.partition { |issue| issue.is_a?(ExternalIssue) } + + data = Entities::IssueBasic.represent(internal_issues, current_user: current_user) + data += Entities::ExternalIssue.represent(external_issues, current_user: current_user) + + data.as_json end end end diff --git a/lib/api/milestone_responses.rb b/lib/api/milestone_responses.rb new file mode 100644 index 00000000000..ef09d9505d2 --- /dev/null +++ b/lib/api/milestone_responses.rb @@ -0,0 +1,98 @@ +module API + module MilestoneResponses + extend ActiveSupport::Concern + + included do + helpers do + params :optional_params do + optional :description, type: String, desc: 'The description of the milestone' + optional :due_date, type: String, desc: 'The due date of the milestone. The ISO 8601 date format (%Y-%m-%d)' + optional :start_date, type: String, desc: 'The start date of the milestone. The ISO 8601 date format (%Y-%m-%d)' + end + + params :list_params do + optional :state, type: String, values: %w[active closed all], default: 'all', + desc: 'Return "active", "closed", or "all" milestones' + optional :iids, type: Array[Integer], desc: 'The IIDs of the milestones' + optional :search, type: String, desc: 'The search criteria for the title or description of the milestone' + use :pagination + end + + params :update_params do + requires :milestone_id, type: Integer, desc: 'The milestone ID number' + optional :title, type: String, desc: 'The title of the milestone' + optional :state_event, type: String, values: %w[close activate], + desc: 'The state event of the milestone ' + use :optional_params + at_least_one_of :title, :description, :due_date, :state_event + end + + def list_milestones_for(parent) + milestones = parent.milestones + milestones = Milestone.filter_by_state(milestones, params[:state]) + 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 + + def get_milestone_for(parent) + milestone = parent.milestones.find(params[:milestone_id]) + present milestone, with: Entities::Milestone + end + + def create_milestone_for(parent) + milestone = ::Milestones::CreateService.new(parent, current_user, declared_params).execute + + if milestone.valid? + present milestone, with: Entities::Milestone + else + render_api_error!("Failed to create milestone #{milestone.errors.messages}", 400) + end + end + + def update_milestone_for(parent) + milestone = parent.milestones.find(params.delete(:milestone_id)) + + milestone_params = declared_params(include_missing: false) + milestone = ::Milestones::UpdateService.new(parent, current_user, milestone_params).execute(milestone) + + if milestone.valid? + present milestone, with: Entities::Milestone + else + render_api_error!("Failed to update milestone #{milestone.errors.messages}", 400) + end + end + + def milestone_issuables_for(parent, type) + milestone = parent.milestones.find(params[:milestone_id]) + + finder_klass, entity = get_finder_and_entity(type) + + params = build_finder_params(milestone, parent) + + issuables = finder_klass.new(current_user, params).execute + present paginate(issuables), with: entity, current_user: current_user + end + + def build_finder_params(milestone, parent) + finder_params = { milestone_title: milestone.title, sort: 'label_priority' } + + if parent.is_a?(Group) + finder_params.merge(group_id: parent.id) + else + finder_params.merge(project_id: parent.id) + end + end + + def get_finder_and_entity(type) + if type == :issue + [IssuesFinder, Entities::IssueBasic] + else + [MergeRequestsFinder, Entities::MergeRequestBasic] + end + end + end + end + end +end diff --git a/lib/api/milestones.rb b/lib/api/milestones.rb deleted file mode 100644 index 3541d3c95fb..00000000000 --- a/lib/api/milestones.rb +++ /dev/null @@ -1,154 +0,0 @@ -module API - 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 - - params :optional_params do - optional :description, type: String, desc: 'The description of the milestone' - optional :due_date, type: String, desc: 'The due date of the milestone. The ISO 8601 date format (%Y-%m-%d)' - optional :start_date, type: String, desc: 'The start date of the milestone. The ISO 8601 date format (%Y-%m-%d)' - 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 Entities::Milestone - end - params do - optional :state, type: String, values: %w[active closed all], default: 'all', - desc: 'Return "active", "closed", or "all" milestones' - optional :iids, type: Array[Integer], desc: 'The IIDs of the milestones' - optional :search, type: String, desc: 'The search criteria for the title or description 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[:iids]) if params[:iids].present? - milestones = filter_by_search(milestones, params[:search]) if params[:search] - - present paginate(milestones), with: Entities::Milestone - end - - desc 'Get a single project milestone' do - success Entities::Milestone - end - params do - requires :milestone_id, type: Integer, desc: 'The ID of a project milestone' - end - get ":id/milestones/:milestone_id" do - authorize! :read_milestone, user_project - - milestone = user_project.milestones.find(params[:milestone_id]) - present milestone, with: Entities::Milestone - end - - desc 'Create a new project milestone' do - success Entities::Milestone - end - params do - requires :title, type: String, desc: 'The title of the milestone' - use :optional_params - end - post ":id/milestones" do - authorize! :admin_milestone, user_project - - milestone = ::Milestones::CreateService.new(user_project, current_user, declared_params).execute - - if milestone.valid? - present milestone, with: Entities::Milestone - else - render_api_error!("Failed to create milestone #{milestone.errors.messages}", 400) - end - end - - desc 'Update an existing project milestone' do - success Entities::Milestone - end - params do - requires :milestone_id, type: Integer, desc: 'The ID of a project milestone' - optional :title, type: String, desc: 'The title of the milestone' - optional :state_event, type: String, values: %w[close activate], - desc: 'The state event of the milestone ' - use :optional_params - at_least_one_of :title, :description, :due_date, :state_event - end - put ":id/milestones/:milestone_id" do - authorize! :admin_milestone, user_project - milestone = user_project.milestones.find(params.delete(:milestone_id)) - - milestone_params = declared_params(include_missing: false) - milestone = ::Milestones::UpdateService.new(user_project, current_user, milestone_params).execute(milestone) - - if milestone.valid? - present milestone, with: Entities::Milestone - else - render_api_error!("Failed to update milestone #{milestone.errors.messages}", 400) - end - end - - desc 'Get all issues for a single project milestone' do - success Entities::IssueBasic - 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, - sort: 'label_priority' - } - - issues = IssuesFinder.new(current_user, finder_params).execute - 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::MergeRequestBasic - end - params do - requires :milestone_id, type: Integer, desc: 'The ID of a project milestone' - use :pagination - end - get ':id/milestones/:milestone_id/merge_requests' 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, - sort: 'label_priority' - } - - merge_requests = MergeRequestsFinder.new(current_user, finder_params).execute - present paginate(merge_requests), - with: Entities::MergeRequestBasic, - current_user: current_user, - project: user_project - end - end - end -end diff --git a/lib/api/project_milestones.rb b/lib/api/project_milestones.rb new file mode 100644 index 00000000000..451998c726a --- /dev/null +++ b/lib/api/project_milestones.rb @@ -0,0 +1,91 @@ +module API + class ProjectMilestones < Grape::API + include PaginationParams + include MilestoneResponses + + before do + authenticate! + 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 Entities::Milestone + end + params do + use :list_params + end + get ":id/milestones" do + authorize! :read_milestone, user_project + + list_milestones_for(user_project) + end + + desc 'Get a single project milestone' do + success Entities::Milestone + end + params do + requires :milestone_id, type: Integer, desc: 'The ID of a project milestone' + end + get ":id/milestones/:milestone_id" do + authorize! :read_milestone, user_project + + get_milestone_for(user_project) + end + + desc 'Create a new project milestone' do + success Entities::Milestone + end + params do + requires :title, type: String, desc: 'The title of the milestone' + use :optional_params + end + post ":id/milestones" do + authorize! :admin_milestone, user_project + + create_milestone_for(user_project) + end + + desc 'Update an existing project milestone' do + success Entities::Milestone + end + params do + use :update_params + end + put ":id/milestones/:milestone_id" do + authorize! :admin_milestone, user_project + + update_milestone_for(user_project) + end + + desc 'Get all issues for a single project milestone' do + success Entities::IssueBasic + 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_issuables_for(user_project, :issue) + end + + desc 'Get all merge requests for a single project milestone' do + detail 'This feature was introduced in GitLab 9.' + success Entities::MergeRequestBasic + end + params do + requires :milestone_id, type: Integer, desc: 'The ID of a project milestone' + use :pagination + end + get ':id/milestones/:milestone_id/merge_requests' do + authorize! :read_milestone, user_project + + milestone_issuables_for(user_project, :merge_request) + end + end + end +end diff --git a/lib/api/services.rb b/lib/api/services.rb index 7488f95a9b7..843c05ae32e 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -313,12 +313,6 @@ module API desc: 'The base URL to the JIRA instance API. Web URL value will be used if not set. E.g., https://jira-api.example.com' }, { - required: true, - name: :project_key, - type: String, - desc: 'The short identifier for your JIRA project, all uppercase, e.g., PROJ' - }, - { required: false, name: :username, type: String, diff --git a/lib/api/settings.rb b/lib/api/settings.rb index b19095d1252..d55a61fa638 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -20,59 +20,6 @@ module API success Entities::ApplicationSetting end params do - # CE - at_least_one_of_ce = [ - :admin_notification_email, - :after_sign_out_path, - :after_sign_up_text, - :akismet_enabled, - :container_registry_token_expire_delay, - :default_artifacts_expire_in, - :default_branch_protection, - :default_group_visibility, - :default_project_visibility, - :default_projects_limit, - :default_snippet_visibility, - :disabled_oauth_sign_in_sources, - :domain_blacklist_enabled, - :domain_whitelist, - :email_author_in_body, - :enabled_git_access_protocol, - :gravatar_enabled, - :help_page_hide_commercial_content, - :help_page_text, - :help_page_support_url, - :home_page_url, - :housekeeping_enabled, - :html_emails_enabled, - :import_sources, - :koding_enabled, - :max_artifacts_size, - :max_attachment_size, - :max_pages_size, - :metrics_enabled, - :plantuml_enabled, - :polling_interval_multiplier, - :recaptcha_enabled, - :repository_checks_enabled, - :repository_storage, - :require_two_factor_authentication, - :restricted_visibility_levels, - :send_user_confirmation_email, - :sentry_enabled, - :clientside_sentry_enabled, - :session_expire_delay, - :shared_runners_enabled, - :sidekiq_throttling_enabled, - :sign_in_text, - :password_authentication_enabled, - :signin_enabled, - :signup_enabled, - :terminal_max_session_time, - :user_default_external, - :user_oauth_applications, - :version_check_enabled - ] optional :default_branch_protection, type: Integer, values: [0, 1, 2], desc: 'Determine if developers can push to master' 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' @@ -151,7 +98,7 @@ module API given clientside_sentry_enabled: ->(val) { val } do requires :clientside_sentry_dsn, type: String, desc: 'Clientside Sentry Data Source Name' end - optional :repository_storage, type: String, desc: 'Storage paths for new projects' + optional :repository_storages, type: Array[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 @@ -174,7 +121,8 @@ module API optional :terminal_max_session_time, type: Integer, desc: 'Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time.' optional :polling_interval_multiplier, type: BigDecimal, desc: 'Interval multiplier used by endpoints that perform polling. Set to 0 to disable polling.' - at_least_one_of(*at_least_one_of_ce) + optional(*::ApplicationSettingsHelper.visible_attributes) + at_least_one_of(*::ApplicationSettingsHelper.visible_attributes) end put "application/settings" do attrs = declared_params(include_missing: false) diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb index cb0619105e1..edfdb63d183 100644 --- a/lib/api/triggers.rb +++ b/lib/api/triggers.rb @@ -15,24 +15,22 @@ module API optional :variables, type: Hash, desc: 'The list of variables to be injected into build' end 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].to_h - unless variables.all? { |key, value| key.is_a?(String) && value.is_a?(String) } + params[:variables] = params[:variables].to_h + unless params[: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.pipeline, with: Entities::Pipeline + project = find_project(params[:id]) + not_found! unless project + + result = Ci::PipelineTriggerService.new(project, nil, params).execute + not_found! unless result + + if result[:http_status] + render_api_error!(result[:message], result[:http_status]) else - errors = 'No pipeline created' - render_api_error!(errors, 400) + present result[:pipeline], with: Entities::Pipeline end end diff --git a/lib/api/v3/entities.rb b/lib/api/v3/entities.rb index 3759250f7f6..773f667abe0 100644 --- a/lib/api/v3/entities.rb +++ b/lib/api/v3/entities.rb @@ -259,11 +259,40 @@ module API expose :job_events, as: :build_events end - class Issue < ::API::Entities::Issue + class ProjectEntity < Grape::Entity + expose :id, :iid + expose(:project_id) { |entity| entity&.project.try(:id) } + expose :title, :description + expose :state, :created_at, :updated_at + end + + class IssueBasic < ProjectEntity + expose :label_names, as: :labels + expose :milestone, using: ::API::Entities::Milestone + expose :assignees, :author, using: ::API::Entities::UserBasic + + expose :assignee, using: ::API::Entities::UserBasic do |issue, options| + issue.assignees.first + end + + expose :user_notes_count + expose :upvotes, :downvotes + expose :due_date + expose :confidential + + expose :web_url do |issue, options| + Gitlab::UrlBuilder.build(issue) + end + end + + class Issue < IssueBasic unexpose :assignees expose :assignee do |issue, options| ::API::Entities::UserBasic.represent(issue.assignees.first, options) end + expose :subscribed do |issue, options| + issue.subscribed?(options[:current_user], options[:project] || issue.project) + end end end end diff --git a/lib/api/v3/project_hooks.rb b/lib/api/v3/project_hooks.rb index 94614bfc8b6..51014591a93 100644 --- a/lib/api/v3/project_hooks.rb +++ b/lib/api/v3/project_hooks.rb @@ -56,7 +56,9 @@ module API use :project_hook_properties end post ":id/hooks" do - hook = user_project.hooks.new(declared_params(include_missing: false)) + attrs = declared_params(include_missing: false) + attrs[:job_events] = attrs.delete(:build_events) if attrs.key?(:build_events) + hook = user_project.hooks.new(attrs) if hook.save present hook, with: ::API::V3::Entities::ProjectHook @@ -77,7 +79,9 @@ module API put ":id/hooks/:hook_id" do hook = user_project.hooks.find(params.delete(:hook_id)) - if hook.update_attributes(declared_params(include_missing: false)) + attrs = declared_params(include_missing: false) + attrs[:job_events] = attrs.delete(:build_events) if attrs.key?(:build_events) + if hook.update_attributes(attrs) present hook, with: ::API::V3::Entities::ProjectHook else error!("Invalid url given", 422) if hook.errors[:url].present? diff --git a/lib/api/v3/triggers.rb b/lib/api/v3/triggers.rb index a23d6b6b48c..e9d4c35307b 100644 --- a/lib/api/v3/triggers.rb +++ b/lib/api/v3/triggers.rb @@ -28,12 +28,13 @@ module API 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 + result = Ci::CreateTriggerRequestService.execute(project, trigger, params[:ref].to_s, variables) + pipeline = result.pipeline + + if pipeline.persisted? + present result.trigger_request, with: ::API::V3::Entities::TriggerRequest else - errors = 'No builds created' - render_api_error!(errors, 400) + render_validation_error!(pipeline) end end diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index f755c99ea4a..ca6d6848d41 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -8,18 +8,9 @@ module Backup # Make sure there is a connection ActiveRecord::Base.connection.reconnect! - # saving additional informations - s = {} - s[:db_version] = "#{ActiveRecord::Migrator.current_version}" - s[:backup_created_at] = Time.now - s[:gitlab_version] = Gitlab::VERSION - s[:tar_version] = tar_version - s[:skipped] = ENV["SKIP"] - tar_file = "#{s[:backup_created_at].strftime('%s_%Y_%m_%d_')}#{s[:gitlab_version]}#{FILE_NAME_SUFFIX}" - Dir.chdir(backup_path) do File.open("#{backup_path}/backup_information.yml", "w+") do |file| - file << s.to_yaml.gsub(/^---\n/, '') + file << backup_information.to_yaml.gsub(/^---\n/, '') end # create archive @@ -33,11 +24,11 @@ module Backup abort 'Backup failed' end - upload(tar_file) + upload end end - def upload(tar_file) + def upload $progress.print "Uploading backup archive to remote storage #{remote_directory} ... " connection_settings = Gitlab.config.backup.upload.connection @@ -48,7 +39,7 @@ module Backup directory = connect_to_remote_directory(connection_settings) - if directory.files.create(key: tar_file, body: File.open(tar_file), public: false, + if directory.files.create(key: remote_target, body: File.open(tar_file), public: false, multipart_chunk_size: Gitlab.config.backup.upload.multipart_chunk_size, encryption: Gitlab.config.backup.upload.encryption, storage_class: Gitlab.config.backup.upload.storage_class) @@ -177,7 +168,8 @@ module Backup end def connect_to_remote_directory(connection_settings) - connection = ::Fog::Storage.new(connection_settings) + # our settings use string keys, but Fog expects symbols + connection = ::Fog::Storage.new(connection_settings.symbolize_keys) # We only attempt to create the directory for local backups. For AWS # and other cloud providers, we cannot guarantee the user will have @@ -193,6 +185,14 @@ module Backup Gitlab.config.backup.upload.remote_directory end + def remote_target + if ENV['DIRECTORY'] + File.join(ENV['DIRECTORY'], tar_file) + else + tar_file + end + end + def backup_contents folders_to_backup + archives_to_backup + ["backup_information.yml"] end @@ -214,5 +214,19 @@ module Backup def settings @settings ||= YAML.load_file("backup_information.yml") end + + def tar_file + @tar_file ||= "#{backup_information[:backup_created_at].strftime('%s_%Y_%m_%d_')}#{backup_information[:gitlab_version]}#{FILE_NAME_SUFFIX}" + end + + def backup_information + @backup_information ||= { + db_version: ActiveRecord::Migrator.current_version.to_s, + backup_created_at: Time.now, + gitlab_version: Gitlab::VERSION, + tar_version: tar_version, + skipped: ENV["SKIP"] + } + end end end diff --git a/lib/banzai/filter/gollum_tags_filter.rb b/lib/banzai/filter/gollum_tags_filter.rb index 0ea4eeaed5b..2e259904673 100644 --- a/lib/banzai/filter/gollum_tags_filter.rb +++ b/lib/banzai/filter/gollum_tags_filter.rb @@ -118,7 +118,7 @@ module Banzai end if path - content_tag(:img, nil, src: path, class: 'gfm') + content_tag(:img, nil, data: { src: path }, class: 'gfm') end end diff --git a/lib/banzai/filter/image_lazy_load_filter.rb b/lib/banzai/filter/image_lazy_load_filter.rb new file mode 100644 index 00000000000..7a81d583b82 --- /dev/null +++ b/lib/banzai/filter/image_lazy_load_filter.rb @@ -0,0 +1,16 @@ +module Banzai + module Filter + # HTML filter that moves the value of the src attribute to the data-src attribute so it can be lazy loaded + class ImageLazyLoadFilter < HTML::Pipeline::Filter + def call + doc.xpath('descendant-or-self::img').each do |img| + img['class'] ||= '' << 'lazy' + img['data-src'] = img['src'] + img['src'] = LazyImageTagHelper.placeholder_image + end + + doc + end + end + end +end diff --git a/lib/banzai/filter/image_link_filter.rb b/lib/banzai/filter/image_link_filter.rb index 123c92fd250..f318c425962 100644 --- a/lib/banzai/filter/image_link_filter.rb +++ b/lib/banzai/filter/image_link_filter.rb @@ -10,7 +10,7 @@ module Banzai link = doc.document.create_element( 'a', class: 'no-attachment-icon', - href: img['src'], + href: img['data-src'] || img['src'], target: '_blank', rel: 'noopener noreferrer' ) diff --git a/lib/banzai/filter/issue_reference_filter.rb b/lib/banzai/filter/issue_reference_filter.rb index ba1a5ac84b3..ce1ab977d3b 100644 --- a/lib/banzai/filter/issue_reference_filter.rb +++ b/lib/banzai/filter/issue_reference_filter.rb @@ -20,7 +20,7 @@ module Banzai end def url_for_object(issue, project) - IssuesHelper.url_for_issue(issue.iid, project, only_path: context[:only_path]) + IssuesHelper.url_for_issue(issue.iid, project, only_path: context[:only_path], internal: true) end def project_from_ref(ref) diff --git a/lib/banzai/filter/relative_link_filter.rb b/lib/banzai/filter/relative_link_filter.rb index 9e23c8f8c55..c2fed57a0d8 100644 --- a/lib/banzai/filter/relative_link_filter.rb +++ b/lib/banzai/filter/relative_link_filter.rb @@ -22,6 +22,7 @@ module Banzai doc.css('img, video').each do |el| process_link_attr el.attribute('src') + process_link_attr el.attribute('data-src') end doc diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb index bd4d1aa9ff8..3208abfc538 100644 --- a/lib/banzai/pipeline/gfm_pipeline.rb +++ b/lib/banzai/pipeline/gfm_pipeline.rb @@ -16,6 +16,7 @@ module Banzai Filter::MathFilter, Filter::UploadLinkFilter, Filter::VideoLinkFilter, + Filter::ImageLazyLoadFilter, Filter::ImageLinkFilter, Filter::EmojiFilter, Filter::TableOfContentsFilter, diff --git a/lib/banzai/reference_parser/external_issue_parser.rb b/lib/banzai/reference_parser/external_issue_parser.rb index 6307c1b571a..1802cd04854 100644 --- a/lib/banzai/reference_parser/external_issue_parser.rb +++ b/lib/banzai/reference_parser/external_issue_parser.rb @@ -21,10 +21,14 @@ module Banzai gather_attributes_per_project(nodes, self.class.data_attribute) end - private - + # we extract only external issue trackers references here, we don't extract cross-project references, + # so we don't need to do anything here. def can_read_reference?(user, ref_project, node) - can?(user, :read_issue, ref_project) + true + end + + def nodes_visible_to_user(user, nodes) + nodes end end end diff --git a/lib/ci/api/entities.rb b/lib/ci/api/entities.rb index 6b82b2b4f13..31f66dd5a58 100644 --- a/lib/ci/api/entities.rb +++ b/lib/ci/api/entities.rb @@ -52,7 +52,7 @@ module Ci # when old API will be removed (planned for August 2017). model.options.dup.tap do |options| options[:image] = options[:image][:name] if options[:image].is_a?(Hash) - options[:services].map! do |service| + options[:services]&.map! do |service| if service.is_a?(Hash) service[:name] else diff --git a/lib/ci/api/triggers.rb b/lib/ci/api/triggers.rb index 6e622601680..6225203f223 100644 --- a/lib/ci/api/triggers.rb +++ b/lib/ci/api/triggers.rb @@ -24,12 +24,13 @@ module Ci end # create request and trigger builds - trigger_request = Ci::CreateTriggerRequestService.new.execute(project, trigger, params[:ref], variables) - if trigger_request - present trigger_request, with: Entities::TriggerRequest + result = Ci::CreateTriggerRequestService.execute(project, trigger, params[:ref], variables) + pipeline = result.pipeline + + if pipeline.persisted? + present result.trigger_request, with: Entities::TriggerRequest else - errors = 'No builds created' - render_api_error!(errors, 400) + render_validation_error!(pipeline) end end end diff --git a/lib/gitlab/badge/build/metadata.rb b/lib/gitlab/badge/pipeline/metadata.rb index 2ee35a0d4c1..db1e9f8cfb8 100644 --- a/lib/gitlab/badge/build/metadata.rb +++ b/lib/gitlab/badge/pipeline/metadata.rb @@ -1,8 +1,8 @@ module Gitlab module Badge - module Build + module Pipeline ## - # Class that describes build badge metadata + # Class that describes pipeline badge metadata # class Metadata < Badge::Metadata def initialize(badge) @@ -11,11 +11,11 @@ module Gitlab end def title - 'build status' + 'pipeline status' end def image_url - build_project_badges_url(@project, @ref, format: :svg) + pipeline_project_badges_url(@project, @ref, format: :svg) end def link_url diff --git a/lib/gitlab/badge/build/status.rb b/lib/gitlab/badge/pipeline/status.rb index b762d85b6e5..5fee7a93475 100644 --- a/lib/gitlab/badge/build/status.rb +++ b/lib/gitlab/badge/pipeline/status.rb @@ -1,8 +1,8 @@ module Gitlab module Badge - module Build + module Pipeline ## - # Build status badge + # Pipeline status badge # class Status < Badge::Base attr_reader :project, :ref @@ -15,7 +15,7 @@ module Gitlab end def entity - 'build' + 'pipeline' end def status @@ -25,11 +25,11 @@ module Gitlab end def metadata - @metadata ||= Build::Metadata.new(self) + @metadata ||= Pipeline::Metadata.new(self) end def template - @template ||= Build::Template.new(self) + @template ||= Pipeline::Template.new(self) end end end diff --git a/lib/gitlab/badge/build/template.rb b/lib/gitlab/badge/pipeline/template.rb index bc0e0cd441d..e09db32262d 100644 --- a/lib/gitlab/badge/build/template.rb +++ b/lib/gitlab/badge/pipeline/template.rb @@ -1,12 +1,13 @@ module Gitlab module Badge - module Build + module Pipeline ## - # Class that represents a build badge template. + # Class that represents a pipeline badge template. # # Template object will be passed to badge.svg.erb template. # class Template < Badge::Template + STATUS_RENAME = { 'success' => 'passed' }.freeze STATUS_COLOR = { success: '#4c1', failed: '#e05d44', @@ -27,11 +28,11 @@ module Gitlab end def value_text - @status.to_s + STATUS_RENAME[@status.to_s] || @status.to_s end def key_width - 38 + 62 end def value_width diff --git a/lib/gitlab/ci/trace/stream.rb b/lib/gitlab/ci/trace/stream.rb index 5d6977106d6..8503ecf8700 100644 --- a/lib/gitlab/ci/trace/stream.rb +++ b/lib/gitlab/ci/trace/stream.rb @@ -67,13 +67,14 @@ module Gitlab def extract_coverage(regex) return unless valid? - return unless regex + return unless regex.present? regex = Gitlab::UntrustedRegexp.new(regex) match = "" reverse_line do |line| + line.chomp! matches = regex.scan(line) next unless matches.is_a?(Array) next if matches.empty? diff --git a/lib/gitlab/data_builder/push.rb b/lib/gitlab/data_builder/push.rb index 8c8729b6557..5c5f507d44d 100644 --- a/lib/gitlab/data_builder/push.rb +++ b/lib/gitlab/data_builder/push.rb @@ -24,11 +24,11 @@ module Gitlab # total_commits_count: Fixnum # } # - def build(project, user, oldrev, newrev, ref, commits = [], message = nil) + def build(project, user, oldrev, newrev, ref, commits = [], message = nil, commits_count: nil) commits = Array(commits) # Total commits count - commits_count = commits.size + commits_count ||= commits.size # Get latest 20 commits ASC commits_limited = commits.last(20) diff --git a/lib/gitlab/devise_failure.rb b/lib/gitlab/devise_failure.rb deleted file mode 100644 index a78fde9d782..00000000000 --- a/lib/gitlab/devise_failure.rb +++ /dev/null @@ -1,23 +0,0 @@ -module Gitlab - class DeviseFailure < Devise::FailureApp - protected - - # Override `Devise::FailureApp#request_format` to handle a special case - # - # This tells Devise to handle an unauthenticated `.zip` request as an HTML - # request (i.e., redirect to sign in). - # - # Otherwise, Devise would respond with a 401 Unauthorized with - # `Content-Type: application/zip` and a response body in plaintext, and the - # browser would freak out. - # - # See https://gitlab.com/gitlab-org/gitlab-ce/issues/12944 - def request_format - if request.format == :zip - Mime::Type.lookup_by_extension(:html).ref - else - super - end - end - end -end diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb index 4175746be39..b6449f27034 100644 --- a/lib/gitlab/git.rb +++ b/lib/gitlab/git.rb @@ -10,7 +10,7 @@ module Gitlab include Gitlab::EncodingHelper def ref_name(ref) - encode! ref.sub(/\Arefs\/(tags|heads)\//, '') + encode! ref.sub(/\Arefs\/(tags|heads|remotes)\//, '') end def branch_name(ref) diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb index 09511cc6504..ca7e3a7c4be 100644 --- a/lib/gitlab/git/commit.rb +++ b/lib/gitlab/git/commit.rb @@ -319,6 +319,15 @@ module Gitlab end end + # Get the gpg signature of this commit. + # + # Ex. + # commit.signature(repo) + # + def signature(repo) + Rugged::Commit.extract_signature(repo.rugged, sha) + end + def stats Gitlab::Git::CommitStats.new(self) end @@ -327,7 +336,7 @@ module Gitlab begin raw_commit.to_mbox(options) rescue Rugged::InvalidError => ex - if ex.message =~ /Commit \w+ is a merge commit/ + if ex.message =~ /commit \w+ is a merge commit/i 'Patch format is not currently supported for merge commits.' end end diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 63eebadff2e..a3bc79109f8 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -45,6 +45,8 @@ module Gitlab :bare?, to: :rugged + delegate :exists?, to: :gitaly_repository_client + # Default branch in the repository def root_ref @root_ref ||= gitaly_migrate(:root_ref) do |is_enabled| @@ -80,10 +82,14 @@ module Gitlab end # Returns an Array of Branches - # - # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/389 - def branches(sort_by: nil) - branches_filter(sort_by: sort_by) + def branches + gitaly_migrate(:branches) do |is_enabled| + if is_enabled + gitaly_ref_client.branches + else + branches_filter + end + end end def reload_rugged @@ -162,20 +168,13 @@ module Gitlab # # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/390 def tags - rugged.references.each("refs/tags/*").map do |ref| - message = nil - - if ref.target.is_a?(Rugged::Tag::Annotation) - tag_message = ref.target.message - - if tag_message.respond_to?(:chomp) - message = tag_message.chomp - end + gitaly_migrate(:tags) do |is_enabled| + if is_enabled + tags_from_gitaly + else + tags_from_rugged end - - target_commit = Gitlab::Git::Commit.find(self, ref.target) - Gitlab::Git::Tag.new(self, ref.name, ref.target, target_commit, message) - end.sort_by(&:name) + end end # Returns true if the given tag exists @@ -208,10 +207,6 @@ module Gitlab !empty? end - def repo_exists? - !!rugged - end - # Discovers the default branch based on the repository's available branches # # - If no branches are present, returns nil @@ -480,20 +475,6 @@ module Gitlab end end - # Sets HEAD to the commit specified by +ref+; +ref+ can be a branch or - # tag name or a commit SHA. Valid +reset_type+ values are: - # - # [:soft] - # the head will be moved to the commit. - # [:mixed] - # will trigger a +:soft+ reset, plus the index will be replaced - # with the content of the commit tree. - # [:hard] - # will trigger a +:mixed+ reset and the working directory will be - # replaced with the content of the index. (Untracked and ignored files - # will be left alone) - delegate :reset, to: :rugged - # Mimic the `git clean` command and recursively delete untracked files. # Valid keys that can be passed in the +options+ hash are: # @@ -518,154 +499,6 @@ module Gitlab # TODO: implement this method end - # Check out the specified ref. Valid options are: - # - # :b - Create a new branch at +start_point+ and set HEAD to the new - # branch. - # - # * These options are passed to the Rugged::Repository#checkout method: - # - # :progress :: - # A callback that will be executed for checkout progress notifications. - # Up to 3 parameters are passed on each execution: - # - # - The path to the last updated file (or +nil+ on the very first - # invocation). - # - The number of completed checkout steps. - # - The number of total checkout steps to be performed. - # - # :notify :: - # A callback that will be executed for each checkout notification - # types specified with +:notify_flags+. Up to 5 parameters are passed - # on each execution: - # - # - An array containing the +:notify_flags+ that caused the callback - # execution. - # - The path of the current file. - # - A hash describing the baseline blob (or +nil+ if it does not - # exist). - # - A hash describing the target blob (or +nil+ if it does not exist). - # - A hash describing the workdir blob (or +nil+ if it does not - # exist). - # - # :strategy :: - # A single symbol or an array of symbols representing the strategies - # to use when performing the checkout. Possible values are: - # - # :none :: - # Perform a dry run (default). - # - # :safe :: - # Allow safe updates that cannot overwrite uncommitted data. - # - # :safe_create :: - # Allow safe updates plus creation of missing files. - # - # :force :: - # Allow all updates to force working directory to look like index. - # - # :allow_conflicts :: - # Allow checkout to make safe updates even if conflicts are found. - # - # :remove_untracked :: - # Remove untracked files not in index (that are not ignored). - # - # :remove_ignored :: - # Remove ignored files not in index. - # - # :update_only :: - # Only update existing files, don't create new ones. - # - # :dont_update_index :: - # Normally checkout updates index entries as it goes; this stops - # that. - # - # :no_refresh :: - # Don't refresh index/config/etc before doing checkout. - # - # :disable_pathspec_match :: - # Treat pathspec as simple list of exact match file paths. - # - # :skip_locked_directories :: - # Ignore directories in use, they will be left empty. - # - # :skip_unmerged :: - # Allow checkout to skip unmerged files (NOT IMPLEMENTED). - # - # :use_ours :: - # For unmerged files, checkout stage 2 from index (NOT IMPLEMENTED). - # - # :use_theirs :: - # For unmerged files, checkout stage 3 from index (NOT IMPLEMENTED). - # - # :update_submodules :: - # Recursively checkout submodules with same options (NOT - # IMPLEMENTED). - # - # :update_submodules_if_changed :: - # Recursively checkout submodules if HEAD moved in super repo (NOT - # IMPLEMENTED). - # - # :disable_filters :: - # If +true+, filters like CRLF line conversion will be disabled. - # - # :dir_mode :: - # Mode for newly created directories. Default: +0755+. - # - # :file_mode :: - # Mode for newly created files. Default: +0755+ or +0644+. - # - # :file_open_flags :: - # Mode for opening files. Default: - # <code>IO::CREAT | IO::TRUNC | IO::WRONLY</code>. - # - # :notify_flags :: - # A single symbol or an array of symbols representing the cases in - # which the +:notify+ callback should be invoked. Possible values are: - # - # :none :: - # Do not invoke the +:notify+ callback (default). - # - # :conflict :: - # Invoke the callback for conflicting paths. - # - # :dirty :: - # Invoke the callback for "dirty" files, i.e. those that do not need - # an update but no longer match the baseline. - # - # :updated :: - # Invoke the callback for any file that was changed. - # - # :untracked :: - # Invoke the callback for untracked files. - # - # :ignored :: - # Invoke the callback for ignored files. - # - # :all :: - # Invoke the callback for all these cases. - # - # :paths :: - # A glob string or an array of glob strings specifying which paths - # should be taken into account for the checkout operation. +nil+ will - # match all files. Default: +nil+. - # - # :baseline :: - # A Rugged::Tree that represents the current, expected contents of the - # workdir. Default: +HEAD+. - # - # :target_directory :: - # A path to an alternative workdir directory in which the checkout - # should be performed. - def checkout(ref, options = {}, start_point = "HEAD") - if options[:b] - rugged.branches.create(ref, start_point) - options.delete(:b) - end - default_options = { strategy: [:recreate_missing, :safe] } - rugged.checkout(ref, default_options.merge(options)) - end - # Delete the specified branch from the repository def delete_branch(branch_name) rugged.branches.delete(branch_name) @@ -815,6 +648,10 @@ module Gitlab @gitaly_commit_client ||= Gitlab::GitalyClient::CommitService.new(self) end + def gitaly_repository_client + @gitaly_repository_client ||= Gitlab::GitalyClient::RepositoryService.new(self) + end + private # Gitaly note: JV: Trying to get rid of the 'filter' option so we can implement this with 'git'. @@ -1113,6 +950,27 @@ module Gitlab end end + def tags_from_rugged + rugged.references.each("refs/tags/*").map do |ref| + message = nil + + if ref.target.is_a?(Rugged::Tag::Annotation) + tag_message = ref.target.message + + if tag_message.respond_to?(:chomp) + message = tag_message.chomp + end + end + + target_commit = Gitlab::Git::Commit.find(self, ref.target) + Gitlab::Git::Tag.new(self, ref.name, ref.target, target_commit, message) + end.sort_by(&:name) + end + + def tags_from_gitaly + gitaly_ref_client.tags + end + def gitaly_migrate(method, &block) Gitlab::GitalyClient.migrate(method, &block) rescue GRPC::NotFound => e diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb index 435e41e36fb..c90ef282fdd 100644 --- a/lib/gitlab/gitaly_client.rb +++ b/lib/gitlab/gitaly_client.rb @@ -57,7 +57,7 @@ module Gitlab metadata = yield(metadata) if block_given? stub(service, storage).send(rpc, request, metadata) end - + def self.request_metadata(storage) encoded_token = Base64.strict_encode64(token(storage).to_s) { metadata: { 'authorization' => "Bearer #{encoded_token}" } } diff --git a/lib/gitlab/gitaly_client/ref_service.rb b/lib/gitlab/gitaly_client/ref_service.rb index 2c3d53410ac..b0f7548b7dc 100644 --- a/lib/gitlab/gitaly_client/ref_service.rb +++ b/lib/gitlab/gitaly_client/ref_service.rb @@ -10,6 +10,19 @@ module Gitlab @storage = repository.storage end + def branches + request = Gitaly::FindAllBranchesRequest.new(repository: @gitaly_repo) + response = GitalyClient.call(@storage, :ref_service, :find_all_branches, request) + + response.flat_map do |message| + message.branches.map do |branch| + gitaly_commit = GitalyClient::Commit.new(@repository, branch.target) + target_commit = Gitlab::Git::Commit.decorate(gitaly_commit) + Gitlab::Git::Branch.new(@repository, branch.name, branch.target.id, target_commit) + end + end + end + def default_branch_name request = Gitaly::FindDefaultBranchNameRequest.new(repository: @gitaly_repo) response = GitalyClient.call(@storage, :ref_service, :find_default_branch_name, request) @@ -52,6 +65,12 @@ module Gitlab consume_branches_response(response) end + def tags + request = Gitaly::FindAllTagsRequest.new(repository: @gitaly_repo) + response = GitalyClient.call(@storage, :ref_service, :find_all_tags, request) + consume_tags_response(response) + end + private def consume_refs_response(response) @@ -79,6 +98,25 @@ module Gitlab end end + def consume_tags_response(response) + response.flat_map do |message| + message.tags.map do |gitaly_tag| + if gitaly_tag.target_commit.present? + commit = GitalyClient::Commit.new(@repository, gitaly_tag.target_commit) + gitaly_commit = Gitlab::Git::Commit.new(commit) + end + + Gitlab::Git::Tag.new( + @repository, + encode!(gitaly_tag.name.dup), + gitaly_tag.id, + gitaly_commit, + encode!(gitaly_tag.message.chomp) + ) + end + end + end + def commit_from_local_branches_response(response) # Git messages have no encoding enforcements. However, in the UI we only # handle UTF-8, so basically we cross our fingers that the message force diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb new file mode 100644 index 00000000000..f5d84ea8762 --- /dev/null +++ b/lib/gitlab/gitaly_client/repository_service.rb @@ -0,0 +1,16 @@ +module Gitlab + module GitalyClient + class RepositoryService + def initialize(repository) + @repository = repository + @gitaly_repo = repository.gitaly_repository + end + + def exists? + request = Gitaly::RepositoryExistsRequest.new(repository: @gitaly_repo) + + GitalyClient.call(@repository.storage, :repository_service, :exists, request).exists + end + end + end +end diff --git a/lib/gitlab/gpg.rb b/lib/gitlab/gpg.rb new file mode 100644 index 00000000000..e1d1724295a --- /dev/null +++ b/lib/gitlab/gpg.rb @@ -0,0 +1,62 @@ +module Gitlab + module Gpg + extend self + + module CurrentKeyChain + extend self + + def add(key) + GPGME::Key.import(key) + end + + def fingerprints_from_key(key) + import = GPGME::Key.import(key) + + return [] if import.imported == 0 + + import.imports.map(&:fingerprint) + end + end + + def fingerprints_from_key(key) + using_tmp_keychain do + CurrentKeyChain.fingerprints_from_key(key) + end + end + + def primary_keyids_from_key(key) + using_tmp_keychain do + fingerprints = CurrentKeyChain.fingerprints_from_key(key) + + GPGME::Key.find(:public, fingerprints).map { |raw_key| raw_key.primary_subkey.keyid } + end + end + + def user_infos_from_key(key) + using_tmp_keychain do + fingerprints = CurrentKeyChain.fingerprints_from_key(key) + + GPGME::Key.find(:public, fingerprints).flat_map do |raw_key| + raw_key.uids.map { |uid| { name: uid.name, email: uid.email } } + end + end + end + + def using_tmp_keychain + Dir.mktmpdir do |dir| + @original_dirs ||= [GPGME::Engine.dirinfo('homedir')] + @original_dirs.push(dir) + + GPGME::Engine.home_dir = dir + + return_value = yield + + @original_dirs.pop + + GPGME::Engine.home_dir = @original_dirs[-1] + + return_value + end + end + end +end diff --git a/lib/gitlab/gpg/commit.rb b/lib/gitlab/gpg/commit.rb new file mode 100644 index 00000000000..55428b85207 --- /dev/null +++ b/lib/gitlab/gpg/commit.rb @@ -0,0 +1,85 @@ +module Gitlab + module Gpg + class Commit + attr_reader :commit + + def initialize(commit) + @commit = commit + + @signature_text, @signed_text = commit.raw.signature(commit.project.repository) + end + + def has_signature? + !!(@signature_text && @signed_text) + end + + def signature + return unless has_signature? + + cached_signature = GpgSignature.find_by(commit_sha: commit.sha) + return cached_signature if cached_signature.present? + + using_keychain do |gpg_key| + create_cached_signature!(gpg_key) + end + end + + def update_signature!(cached_signature) + using_keychain do |gpg_key| + cached_signature.update_attributes!(attributes(gpg_key)) + end + end + + private + + def using_keychain + Gitlab::Gpg.using_tmp_keychain do + # first we need to get the keyid from the signature to query the gpg + # key belonging to the keyid. + # This way we can add the key to the temporary keychain and extract + # the proper signature. + gpg_key = GpgKey.find_by(primary_keyid: verified_signature.fingerprint) + + if gpg_key + Gitlab::Gpg::CurrentKeyChain.add(gpg_key.key) + @verified_signature = nil + end + + yield gpg_key + end + end + + def verified_signature + @verified_signature ||= GPGME::Crypto.new.verify(@signature_text, signed_text: @signed_text) do |verified_signature| + break verified_signature + end + end + + def create_cached_signature!(gpg_key) + GpgSignature.create!(attributes(gpg_key)) + end + + def attributes(gpg_key) + user_infos = user_infos(gpg_key) + + { + commit_sha: commit.sha, + project: commit.project, + gpg_key: gpg_key, + gpg_key_primary_keyid: gpg_key&.primary_keyid || verified_signature.fingerprint, + gpg_key_user_name: user_infos[:name], + gpg_key_user_email: user_infos[:email], + valid_signature: gpg_signature_valid_signature_value(gpg_key) + } + end + + def gpg_signature_valid_signature_value(gpg_key) + !!(gpg_key && gpg_key.verified? && verified_signature.valid?) + end + + def user_infos(gpg_key) + gpg_key&.verified_user_infos&.first || gpg_key&.user_infos&.first || {} + end + end + end +end diff --git a/lib/gitlab/gpg/invalid_gpg_signature_updater.rb b/lib/gitlab/gpg/invalid_gpg_signature_updater.rb new file mode 100644 index 00000000000..3bb491120ba --- /dev/null +++ b/lib/gitlab/gpg/invalid_gpg_signature_updater.rb @@ -0,0 +1,19 @@ +module Gitlab + module Gpg + class InvalidGpgSignatureUpdater + def initialize(gpg_key) + @gpg_key = gpg_key + end + + def run + GpgSignature + .select(:id, :commit_sha, :project_id) + .where('gpg_key_id IS NULL OR valid_signature = ?', false) + .where(gpg_key_primary_keyid: @gpg_key.primary_keyid) + .find_each do |gpg_signature| + Gitlab::Gpg::Commit.new(gpg_signature.commit).update_signature!(gpg_signature) + end + end + end + end +end diff --git a/lib/gitlab/health_checks/base_abstract_check.rb b/lib/gitlab/health_checks/base_abstract_check.rb index 7de6d4d9367..8b365dab185 100644 --- a/lib/gitlab/health_checks/base_abstract_check.rb +++ b/lib/gitlab/health_checks/base_abstract_check.rb @@ -27,10 +27,10 @@ module Gitlab Metric.new(name, value, labels) end - def with_timing(proc) + def with_timing start = Time.now - result = proc.call - yield result, Time.now.to_f - start.to_f + result = yield + [result, Time.now.to_f - start.to_f] end def catch_timeout(seconds, &block) diff --git a/lib/gitlab/health_checks/fs_shards_check.rb b/lib/gitlab/health_checks/fs_shards_check.rb index bebde857b16..9e91c135956 100644 --- a/lib/gitlab/health_checks/fs_shards_check.rb +++ b/lib/gitlab/health_checks/fs_shards_check.rb @@ -10,47 +10,45 @@ module Gitlab def readiness repository_storages.map do |storage_name| begin - tmp_file_path = tmp_file_path(storage_name) - if !storage_stat_test(storage_name) HealthChecks::Result.new(false, 'cannot stat storage', shard: storage_name) - elsif !storage_write_test(tmp_file_path) - HealthChecks::Result.new(false, 'cannot write to storage', shard: storage_name) - elsif !storage_read_test(tmp_file_path) - HealthChecks::Result.new(false, 'cannot read from storage', shard: storage_name) else - HealthChecks::Result.new(true, nil, shard: storage_name) + with_temp_file(storage_name) do |tmp_file_path| + if !storage_write_test(tmp_file_path) + HealthChecks::Result.new(false, 'cannot write to storage', shard: storage_name) + elsif !storage_read_test(tmp_file_path) + HealthChecks::Result.new(false, 'cannot read from storage', shard: storage_name) + else + HealthChecks::Result.new(true, nil, shard: storage_name) + end + end end rescue RuntimeError => ex message = "unexpected error #{ex} when checking storage #{storage_name}" Rails.logger.error(message) HealthChecks::Result.new(false, message, shard: storage_name) - ensure - delete_test_file(tmp_file_path) end end end def metrics repository_storages.flat_map do |storage_name| - tmp_file_path = tmp_file_path(storage_name) [ - operation_metrics(:filesystem_accessible, :filesystem_access_latency_seconds, -> { storage_stat_test(storage_name) }, shard: storage_name), - operation_metrics(:filesystem_writable, :filesystem_write_latency_seconds, -> { storage_write_test(tmp_file_path) }, shard: storage_name), - operation_metrics(:filesystem_readable, :filesystem_read_latency_seconds, -> { storage_read_test(tmp_file_path) }, shard: storage_name) + storage_stat_metrics(storage_name), + storage_write_metrics(storage_name), + storage_read_metrics(storage_name) ].flatten end end private - def operation_metrics(ok_metric, latency_metric, operation, **labels) - with_timing operation do |result, elapsed| - [ - metric(latency_metric, elapsed, **labels), - metric(ok_metric, result ? 1 : 0, **labels) - ] - end + def operation_metrics(ok_metric, latency_metric, **labels) + result, elapsed = yield + [ + metric(latency_metric, elapsed, **labels), + metric(ok_metric, result ? 1 : 0, **labels) + ] rescue RuntimeError => ex Rails.logger.error("unexpected error #{ex} when checking #{ok_metric}") [metric(ok_metric, 0, **labels)] @@ -68,19 +66,36 @@ module Gitlab Gitlab::Popen.popen([TIMEOUT_EXECUTABLE, COMMAND_TIMEOUT].concat(cmd_args), *args, &block) end - def tmp_file_path(storage_name) - Dir::Tmpname.create(%w(fs_shards_check +deleted), path(storage_name)) { |path| path } + def with_temp_file(storage_name) + temp_file_path = Dir::Tmpname.create(%w(fs_shards_check +deleted), storage_path(storage_name)) { |path| path } + yield temp_file_path + ensure + delete_test_file(temp_file_path) end - def path(storage_name) + def storage_path(storage_name) storages_paths&.dig(storage_name, 'path') end + # All below test methods use shell commands to perform actions on storage volumes. + # In case a storage volume have connectivity problems causing pure Ruby IO operation to wait indefinitely, + # we can rely on shell commands to be terminated once `timeout` kills them. + # + # However we also fallback to pure Ruby file operations in case a specific shell command is missing + # so we are still able to perform healthchecks and gather metrics from such system. + + def delete_test_file(tmp_path) + _, status = exec_with_timeout(%W{ rm -f #{tmp_path} }) + status.zero? + rescue Errno::ENOENT + File.delete(tmp_path) rescue Errno::ENOENT + end + def storage_stat_test(storage_name) - stat_path = File.join(path(storage_name), '.') + stat_path = File.join(storage_path(storage_name), '.') begin _, status = exec_with_timeout(%W{ stat #{stat_path} }) - status == 0 + status.zero? rescue Errno::ENOENT File.exist?(stat_path) && File::Stat.new(stat_path).readable? end @@ -90,7 +105,7 @@ module Gitlab _, status = exec_with_timeout(%W{ tee #{tmp_path} }) do |stdin| stdin.write(RANDOM_STRING) end - status == 0 + status.zero? rescue Errno::ENOENT written_bytes = File.write(tmp_path, RANDOM_STRING) rescue Errno::ENOENT written_bytes == RANDOM_STRING.length @@ -100,17 +115,33 @@ module Gitlab _, status = exec_with_timeout(%W{ diff #{tmp_path} - }) do |stdin| stdin.write(RANDOM_STRING) end - status == 0 + status.zero? rescue Errno::ENOENT file_contents = File.read(tmp_path) rescue Errno::ENOENT file_contents == RANDOM_STRING end - def delete_test_file(tmp_path) - _, status = exec_with_timeout(%W{ rm -f #{tmp_path} }) - status == 0 - rescue Errno::ENOENT - File.delete(tmp_path) rescue Errno::ENOENT + def storage_stat_metrics(storage_name) + operation_metrics(:filesystem_accessible, :filesystem_access_latency_seconds, shard: storage_name) do + with_timing { storage_stat_test(storage_name) } + end + end + + def storage_write_metrics(storage_name) + operation_metrics(:filesystem_writable, :filesystem_write_latency_seconds, shard: storage_name) do + with_temp_file(storage_name) do |tmp_file_path| + with_timing { storage_write_test(tmp_file_path) } + end + end + end + + def storage_read_metrics(storage_name) + operation_metrics(:filesystem_readable, :filesystem_read_latency_seconds, shard: storage_name) do + with_temp_file(storage_name) do |tmp_file_path| + storage_write_test(tmp_file_path) # writes data used by read test + with_timing { storage_read_test(tmp_file_path) } + end + end end end end diff --git a/lib/gitlab/health_checks/simple_abstract_check.rb b/lib/gitlab/health_checks/simple_abstract_check.rb index 3dcb28a193c..f5026171ba4 100644 --- a/lib/gitlab/health_checks/simple_abstract_check.rb +++ b/lib/gitlab/health_checks/simple_abstract_check.rb @@ -15,14 +15,13 @@ module Gitlab end def metrics - with_timing method(:check) do |result, elapsed| - Rails.logger.error("#{human_name} check returned unexpected result #{result}") unless is_successful?(result) - [ - metric("#{metric_prefix}_timeout", result.is_a?(Timeout::Error) ? 1 : 0), - metric("#{metric_prefix}_success", is_successful?(result) ? 1 : 0), - metric("#{metric_prefix}_latency_seconds", elapsed) - ] - end + result, elapsed = with_timing(&method(:check)) + Rails.logger.error("#{human_name} check returned unexpected result #{result}") unless is_successful?(result) + [ + metric("#{metric_prefix}_timeout", result.is_a?(Timeout::Error) ? 1 : 0), + metric("#{metric_prefix}_success", is_successful?(result) ? 1 : 0), + metric("#{metric_prefix}_latency_seconds", elapsed) + ] end private diff --git a/lib/gitlab/i18n.rb b/lib/gitlab/i18n.rb index a1b896c9511..cc282d1415b 100644 --- a/lib/gitlab/i18n.rb +++ b/lib/gitlab/i18n.rb @@ -7,10 +7,10 @@ module Gitlab 'es' => 'Español', 'de' => 'Deutsch', 'fr' => 'Français', - 'pt_BR' => 'Português(Brasil)', + 'pt_BR' => 'Português (Brasil)', 'zh_CN' => '简体中文', - 'zh_HK' => '繁體中文(香港)', - 'zh_TW' => '繁體中文(臺灣)', + 'zh_HK' => '繁體中文 (香港)', + 'zh_TW' => '繁體中文 (臺灣)', 'bg' => 'български', 'ru' => 'Русский', 'eo' => 'Esperanto', diff --git a/lib/gitlab/ldap/adapter.rb b/lib/gitlab/ldap/adapter.rb index 7b05290e5cc..8867a91c244 100644 --- a/lib/gitlab/ldap/adapter.rb +++ b/lib/gitlab/ldap/adapter.rb @@ -101,7 +101,7 @@ module Gitlab end def user_attributes - %W(#{config.uid} cn mail dn) + %W(#{config.uid} cn dn) + config.attributes['username'] + config.attributes['email'] end end end diff --git a/lib/gitlab/ldap/config.rb b/lib/gitlab/ldap/config.rb index 6fdf68641e2..c8f19cd52d5 100644 --- a/lib/gitlab/ldap/config.rb +++ b/lib/gitlab/ldap/config.rb @@ -2,6 +2,12 @@ module Gitlab module LDAP class Config + NET_LDAP_ENCRYPTION_METHOD = { + simple_tls: :simple_tls, + start_tls: :start_tls, + plain: nil + }.freeze + attr_accessor :provider, :options def self.enabled? @@ -12,6 +18,12 @@ module Gitlab Gitlab.config.ldap.servers.values end + def self.available_servers + return [] unless enabled? + + Array.wrap(servers.first) + end + def self.providers servers.map { |server| server['provider_name'] } end @@ -39,7 +51,7 @@ module Gitlab def adapter_options opts = base_options.merge( - encryption: encryption + encryption: encryption_options ) opts.merge!(auth_options) if has_auth? @@ -50,9 +62,10 @@ module Gitlab def omniauth_options opts = base_options.merge( base: base, - method: options['method'], + encryption: options['encryption'], filter: omniauth_user_filter, - name_proc: name_proc + name_proc: name_proc, + disable_verify_certificates: !options['verify_certificates'] ) if has_auth? @@ -62,6 +75,9 @@ module Gitlab ) end + opts[:ca_file] = options['ca_file'] if options['ca_file'].present? + opts[:ssl_version] = options['ssl_version'] if options['ssl_version'].present? + opts end @@ -157,15 +173,37 @@ module Gitlab base_config.servers.values.find { |server| server['provider_name'] == provider } end - def encryption - case options['method'].to_s - when 'ssl' - :simple_tls - when 'tls' - :start_tls - else - nil - end + def encryption_options + method = translate_method(options['encryption']) + return nil unless method + + { + method: method, + tls_options: tls_options(method) + } + end + + def translate_method(method_from_config) + NET_LDAP_ENCRYPTION_METHOD[method_from_config.to_sym] + end + + def tls_options(method) + return { verify_mode: OpenSSL::SSL::VERIFY_NONE } unless method + + opts = if options['verify_certificates'] + OpenSSL::SSL::SSLContext::DEFAULT_PARAMS + else + # It is important to explicitly set verify_mode for two reasons: + # 1. The behavior of OpenSSL is undefined when verify_mode is not set. + # 2. The net-ldap gem implementation verifies the certificate hostname + # unless verify_mode is set to VERIFY_NONE. + { verify_mode: OpenSSL::SSL::VERIFY_NONE } + end + + opts[:ca_file] = options['ca_file'] if options['ca_file'].present? + opts[:ssl_version] = options['ssl_version'] if options['ssl_version'].present? + + opts end def auth_options diff --git a/lib/gitlab/path_regex.rb b/lib/gitlab/path_regex.rb index 60a32d5d5ea..894bd5efae5 100644 --- a/lib/gitlab/path_regex.rb +++ b/lib/gitlab/path_regex.rb @@ -14,42 +14,42 @@ module Gitlab TOP_LEVEL_ROUTES = %w[ - .well-known + 404.html + 422.html + 500.html + 502.html + 503.html abuse_reports admin - all api + apple-touch-icon-precomposed.png + apple-touch-icon.png assets autocomplete ci dashboard + deploy.html explore + favicon.ico files groups health_check help - hooks import invites - issues jwt koding - member - merge_requests - new - notes notification_settings oauth profile projects public - repository robots.txt s search sent_notifications - services + slash-command-logo.png snippets - teams u unicorn_test unsubscribes diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb index 7668ecacc4b..f5b757ace77 100644 --- a/lib/gitlab/reference_extractor.rb +++ b/lib/gitlab/reference_extractor.rb @@ -33,7 +33,12 @@ module Gitlab def issues if project && project.jira_tracker? - @references[:external_issue] ||= references(:external_issue) + if project.issues_enabled? + @references[:all_issues] ||= references(:external_issue) + references(:issue) + else + @references[:external_issue] ||= references(:external_issue) + + references(:issue).select { |i| i.project_id != project.id } + end else @references[:issue] ||= references(:issue) end diff --git a/lib/gitlab/request_forgery_protection.rb b/lib/gitlab/request_forgery_protection.rb new file mode 100644 index 00000000000..ccfe0d6bed3 --- /dev/null +++ b/lib/gitlab/request_forgery_protection.rb @@ -0,0 +1,39 @@ +# A module to check CSRF tokens in requests. +# It's used in API helpers and OmniAuth. +# Usage: GitLab::RequestForgeryProtection.call(env) + +module Gitlab + module RequestForgeryProtection + class Controller < ActionController::Base + protect_from_forgery with: :exception + + rescue_from ActionController::InvalidAuthenticityToken do |e| + logger.warn "This CSRF token verification failure is handled internally by `GitLab::RequestForgeryProtection`" + logger.warn "Unlike the logs may suggest, this does not result in an actual 422 response to the user" + logger.warn "For API requests, the only effect is that `current_user` will be `nil` for the duration of the request" + + raise e + end + + def index + head :ok + end + end + + def self.app + @app ||= Controller.action(:index) + end + + def self.call(env) + app.call(env) + end + + def self.verified?(env) + call(env) + + true + rescue ActionController::InvalidAuthenticityToken + false + end + end +end diff --git a/lib/gitlab/slash_commands/issue_command.rb b/lib/gitlab/slash_commands/issue_command.rb index 87ea19b8806..3d96982b820 100644 --- a/lib/gitlab/slash_commands/issue_command.rb +++ b/lib/gitlab/slash_commands/issue_command.rb @@ -2,7 +2,7 @@ module Gitlab module SlashCommands class IssueCommand < BaseCommand def self.available?(project) - project.issues_enabled? && project.default_issues_tracker? + project.issues_enabled? end def collection diff --git a/lib/gitlab/untrusted_regexp.rb b/lib/gitlab/untrusted_regexp.rb index 925b2158a22..7ce2e9d636e 100644 --- a/lib/gitlab/untrusted_regexp.rb +++ b/lib/gitlab/untrusted_regexp.rb @@ -22,28 +22,9 @@ module Gitlab end def scan(text) - text = text.dup # modified in-place - results = [] - - loop do - match = scan_regexp.match(text) - break unless match - - # Ruby scan returns empty strings, not nil - groups = match.to_a.map(&:to_s) - - results << - if regexp.number_of_capturing_groups.zero? - groups[0] - else - groups[1..-1] - end - - text.slice!(0, match.end(0) || 1) - break unless text.present? - end - - results + matches = scan_regexp.scan(text).to_a + matches.map!(&:first) if regexp.number_of_capturing_groups.zero? + matches end def replace(text, rewrite) diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb index dba071d7e47..e0ac21305a5 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -40,14 +40,13 @@ module Gitlab pages_domains: PagesDomain.count, projects: Project.count, projects_imported_from_github: Project.where(import_type: 'github').count, - projects_prometheus_active: PrometheusService.active.count, protected_branches: ProtectedBranch.count, releases: Release.count, snippets: Snippet.count, todos: Todo.count, uploads: Upload.count, web_hooks: WebHook.count - } + }.merge(services_usage) } end @@ -64,6 +63,18 @@ module Gitlab usage_data end + + def services_usage + types = { + JiraService: :projects_jira_active, + SlackService: :projects_slack_notifications_active, + SlackSlashCommandsService: :projects_slack_slash_active, + PrometheusService: :projects_prometheus_active + } + + results = Service.unscoped.where(type: types.keys, active: true).group(:type).count + results.each_with_object({}) { |(key, value), response| response[types[key.to_sym]] = value } + end end end end diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb index 8e91ee7287c..d9a5af09f08 100644 --- a/lib/gitlab/user_access.rb +++ b/lib/gitlab/user_access.rb @@ -37,8 +37,8 @@ module Gitlab request_cache def can_create_tag?(ref) return false unless can_access_git? - if ProtectedTag.protected?(project, ref) - project.protected_tags.protected_ref_accessible_to?(ref, user, action: :create) + if protected?(ProtectedTag, project, ref) + protected_tag_accessible_to?(ref, action: :create) else user.can?(:push_code, project) end @@ -47,20 +47,24 @@ module Gitlab request_cache def can_delete_branch?(ref) return false unless can_access_git? - if ProtectedBranch.protected?(project, ref) + if protected?(ProtectedBranch, project, ref) user.can?(:delete_protected_branch, project) else user.can?(:push_code, project) end end + def can_update_branch?(ref) + can_push_to_branch?(ref) || can_merge_to_branch?(ref) + end + request_cache def can_push_to_branch?(ref) return false unless can_access_git? - if ProtectedBranch.protected?(project, ref) + if protected?(ProtectedBranch, project, ref) return true if project.empty_repo? && project.user_can_push_to_empty_repo?(user) - project.protected_branches.protected_ref_accessible_to?(ref, user, action: :push) + protected_branch_accessible_to?(ref, action: :push) else user.can?(:push_code, project) end @@ -69,8 +73,8 @@ module Gitlab request_cache def can_merge_to_branch?(ref) return false unless can_access_git? - if ProtectedBranch.protected?(project, ref) - project.protected_branches.protected_ref_accessible_to?(ref, user, action: :merge) + if protected?(ProtectedBranch, project, ref) + protected_branch_accessible_to?(ref, action: :merge) else user.can?(:push_code, project) end @@ -87,5 +91,23 @@ module Gitlab def can_access_git? user && user.can?(:access_git) end + + def protected_branch_accessible_to?(ref, action:) + ProtectedBranch.protected_ref_accessible_to?( + ref, user, + action: action, + protected_refs: project.protected_branches) + end + + def protected_tag_accessible_to?(ref, action:) + ProtectedTag.protected_ref_accessible_to?( + ref, user, + action: action, + protected_refs: project.protected_tags) + end + + request_cache def protected?(kind, project, ref) + kind.protected?(project, ref) + end end end diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb index 916ef365d78..3f25e463412 100644 --- a/lib/gitlab/workhorse.rb +++ b/lib/gitlab/workhorse.rb @@ -35,7 +35,10 @@ module Gitlab when 'git_receive_pack' Gitlab::GitalyClient.feature_enabled?(:post_receive_pack) when 'git_upload_pack' - Gitlab::GitalyClient.feature_enabled?(:post_upload_pack) + Gitlab::GitalyClient.feature_enabled?( + :post_upload_pack, + status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT + ) when 'info_refs' true else @@ -62,7 +65,7 @@ module Gitlab end def send_git_blob(repository, blob) - params = if Gitlab::GitalyClient.feature_enabled?(:project_raw_show) + params = if Gitlab::GitalyClient.feature_enabled?(:workhorse_raw_show) { 'GitalyServer' => gitaly_server_hash(repository), 'GetBlobRequest' => { diff --git a/lib/mattermost/client.rb b/lib/mattermost/client.rb index 3d60618006c..d80cd7d2a4e 100644 --- a/lib/mattermost/client.rb +++ b/lib/mattermost/client.rb @@ -24,6 +24,10 @@ module Mattermost json_response session.post(path, options) end + def delete(session, path, options) + json_response session.delete(path, options) + end + def session_get(path, options = {}) with_session do |session| get(session, path, options) @@ -36,6 +40,12 @@ module Mattermost end end + def session_delete(path, options = {}) + with_session do |session| + delete(session, path, options) + end + end + def json_response(response) json_response = JSON.parse(response.body) diff --git a/lib/mattermost/team.rb b/lib/mattermost/team.rb index 2cdbbdece16..b2511f3af1d 100644 --- a/lib/mattermost/team.rb +++ b/lib/mattermost/team.rb @@ -14,5 +14,12 @@ module Mattermost type: type }.to_json) end + + # The deletion is done async, so the response is fast. + # On the mattermost side, this triggers an soft deletion first, after which + # the actuall data is removed + def destroy(team_id:) + session_delete("/api/v4/teams/#{team_id}?permanent=true") + end end end diff --git a/lib/omni_auth/request_forgery_protection.rb b/lib/omni_auth/request_forgery_protection.rb deleted file mode 100644 index 69155131d8d..00000000000 --- a/lib/omni_auth/request_forgery_protection.rb +++ /dev/null @@ -1,21 +0,0 @@ -# Protects OmniAuth request phase against CSRF. - -module OmniAuth - module RequestForgeryProtection - class Controller < ActionController::Base - protect_from_forgery with: :exception - - def index - head :ok - end - end - - def self.app - @app ||= Controller.action(:index) - end - - def self.call(env) - app.call(env) - end - end -end diff --git a/lib/tasks/gitlab/assets.rake b/lib/tasks/gitlab/assets.rake index 003d57adbbd..259a755d724 100644 --- a/lib/tasks/gitlab/assets.rake +++ b/lib/tasks/gitlab/assets.rake @@ -4,6 +4,7 @@ namespace :gitlab do task compile: [ 'yarn:check', 'rake:assets:precompile', + 'gettext:po_to_json', 'webpack:compile', 'fix_urls' ] diff --git a/lib/tasks/gitlab/gitaly.rake b/lib/tasks/gitlab/gitaly.rake index a8db5701d0b..9df07ea8d83 100644 --- a/lib/tasks/gitlab/gitaly.rake +++ b/lib/tasks/gitlab/gitaly.rake @@ -19,7 +19,7 @@ namespace :gitlab do Dir.chdir(args.dir) do create_gitaly_configuration - run_command!([command]) + Bundler.with_original_env { run_command!([command]) } end end diff --git a/lib/tasks/migrate/setup_postgresql.rake b/lib/tasks/migrate/setup_postgresql.rake index 4108cee08b4..9cc986535e1 100644 --- a/lib/tasks/migrate/setup_postgresql.rake +++ b/lib/tasks/migrate/setup_postgresql.rake @@ -4,6 +4,7 @@ require Rails.root.join('db/migrate/20151007120511_namespaces_projects_path_lowe require Rails.root.join('db/migrate/20151008110232_add_users_lower_username_email_indexes') require Rails.root.join('db/migrate/20161212142807_add_lower_path_index_to_routes') require Rails.root.join('db/migrate/20170317203554_index_routes_path_for_like') +require Rails.root.join('db/migrate/20170724214302_add_lower_path_index_to_redirect_routes') require Rails.root.join('db/migrate/20170503185032_index_redirect_routes_path_for_like') desc 'GitLab | Sets up PostgreSQL' @@ -12,5 +13,6 @@ task setup_postgresql: :environment do AddUsersLowerUsernameEmailIndexes.new.up AddLowerPathIndexToRoutes.new.up IndexRoutesPathForLike.new.up + AddLowerPathIndexToRedirectRoutes.new.up IndexRedirectRoutesPathForLike.new.up end |