From e50b3d7d33db05eadeb00a44ca06f1c9bd2393dc Mon Sep 17 00:00:00 2001 From: Praveen Arimbrathodiyil Date: Sun, 15 Jan 2017 13:20:01 +0530 Subject: [PATCH] New upstream version 8.13.11+dfsg --- CHANGELOG.md | 33 ++ Gemfile | 7 +- Gemfile.lock | 20 +- VERSION | 2 +- app/controllers/concerns/creates_commit.rb | 6 +- app/controllers/projects/blob_controller.rb | 20 +- .../projects/branches_controller.rb | 2 +- app/controllers/projects/commit_controller.rb | 16 +- .../projects/commits_controller.rb | 2 +- .../projects/compare_controller.rb | 2 +- .../projects/cycle_analytics_controller.rb | 2 +- .../projects/discussions_controller.rb | 2 +- app/controllers/projects/notes_controller.rb | 2 +- app/controllers/projects/todos_controller.rb | 10 +- app/finders/issuable_finder.rb | 27 +- app/finders/issues_finder.rb | 18 +- app/finders/notes_finder.rb | 111 +++++- app/helpers/commits_helper.rb | 4 +- app/helpers/preferences_helper.rb | 6 +- app/models/commit.rb | 41 +- app/models/concerns/milestoneish.rb | 12 +- app/models/cycle_analytics.rb | 5 +- app/models/cycle_analytics/summary.rb | 5 +- app/models/issue.rb | 55 --- app/models/merge_request.rb | 6 +- app/models/note.rb | 17 - app/models/project.rb | 4 +- app/models/repository.rb | 7 +- app/models/user.rb | 4 - app/services/commits/change_service.rb | 2 +- app/services/git_hooks_service.rb | 6 +- app/services/issuable_base_service.rb | 8 +- app/services/labels/find_or_create_service.rb | 7 +- app/services/merge_requests/build_service.rb | 2 +- app/views/layouts/nav/_project.html.haml | 4 +- app/views/projects/blob/edit.html.haml | 2 +- app/views/projects/commit/_change.html.haml | 2 +- app/views/projects/diffs/_file.html.haml | 2 +- .../merge_requests/_merge_request.html.haml | 2 +- .../projects/merge_requests/_show.html.haml | 2 +- config/application.rb | 5 +- ...30319214458_create_forked_project_links.rb | 2 + ...30506090604_create_deploy_keys_projects.rb | 2 + .../20130617095603_create_users_groups.rb | 2 + ...130711063759_create_project_group_links.rb | 2 + ...0131112114325_create_broadcast_messages.rb | 2 + ...140122112253_create_merge_request_diffs.rb | 2 + db/migrate/20140209025651_create_emails.rb | 4 +- ...140625115202_create_users_star_projects.rb | 2 + db/migrate/20140729134820_create_labels.rb | 2 + .../20140729140420_create_label_links.rb | 2 + .../20140914113604_add_members_table.rb | 2 + ...20140914173417_remove_old_member_tables.rb | 2 + db/migrate/20141118150935_add_audit_event.rb | 2 + ...20141216155758_create_doorkeeper_tables.rb | 2 + ...50108073740_create_application_settings.rb | 2 + ...150313012111_create_subscriptions_table.rb | 6 +- .../20150806104937_create_abuse_reports.rb | 2 + .../20151103134857_create_lfs_objects.rb | 2 + ...51103134958_create_lfs_objects_projects.rb | 2 + db/migrate/20151105094515_create_releases.rb | 2 + db/migrate/20160212123307_create_tasks.rb | 2 + db/migrate/20160416180807_add_award_emoji.rb | 2 + doc/api/services.md | 64 +-- doc/api/users.md | 51 ++- features/steps/shared/authentication.rb | 2 +- lib/api/entities.rb | 6 +- lib/api/helpers.rb | 141 ++++--- lib/api/issues.rb | 76 ++-- lib/api/merge_requests.rb | 49 +-- lib/api/session.rb | 2 +- lib/api/users.rb | 10 +- lib/gitlab/project_search_results.rb | 2 +- lib/gitlab/search_results.rb | 4 +- lib/gitlab/sql/union.rb | 4 +- .../projects/blob_controller_spec.rb | 49 +++ .../projects/branches_controller_spec.rb | 18 + .../projects/todo_controller_spec.rb | 32 +- spec/controllers/search_controller_spec.rb | 61 +++ spec/factories/notes.rb | 2 +- spec/features/projects/blobs/edit_spec.rb | 45 +++ spec/finders/issues_finder_spec.rb | 106 ++++- spec/finders/notes_finder_spec.rb | 200 ++++++++-- spec/fixtures/api/schemas/user/login.json | 37 ++ spec/fixtures/api/schemas/user/public.json | 79 ++++ spec/helpers/preferences_helper_spec.rb | 41 ++ .../lib/gitlab/project_search_results_spec.rb | 38 ++ spec/lib/gitlab/search_results_spec.rb | 73 ++-- spec/lib/gitlab/sql/union_spec.rb | 22 +- spec/models/commit_range_spec.rb | 15 +- spec/models/commit_spec.rb | 11 +- spec/models/cycle_analytics/code_spec.rb | 2 +- spec/models/cycle_analytics/issue_spec.rb | 2 +- spec/models/cycle_analytics/plan_spec.rb | 2 +- .../models/cycle_analytics/production_spec.rb | 2 +- spec/models/cycle_analytics/review_spec.rb | 2 +- spec/models/cycle_analytics/staging_spec.rb | 2 +- spec/models/cycle_analytics/summary_spec.rb | 2 +- spec/models/cycle_analytics/test_spec.rb | 2 +- spec/models/issue_spec.rb | 20 - spec/models/merge_request_spec.rb | 6 +- spec/models/milestone_spec.rb | 11 +- spec/models/note_spec.rb | 38 -- spec/models/project_spec.rb | 16 +- spec/models/repository_spec.rb | 26 ++ spec/models/user_spec.rb | 11 - spec/requests/api/api_helpers_spec.rb | 293 -------------- spec/requests/api/helpers_spec.rb | 373 ++++++++++++++++++ spec/requests/api/issues_spec.rb | 32 +- spec/requests/api/merge_requests_spec.rb | 29 +- spec/requests/api/users_spec.rb | 82 +++- spec/services/labels/transfer_service_spec.rb | 2 +- .../merge_requests/build_service_spec.rb | 12 + .../assets/javascripts/jquery.turbolinks.js | 49 +++ 114 files changed, 1917 insertions(+), 881 deletions(-) create mode 100644 spec/controllers/search_controller_spec.rb create mode 100644 spec/features/projects/blobs/edit_spec.rb create mode 100644 spec/fixtures/api/schemas/user/login.json create mode 100644 spec/fixtures/api/schemas/user/public.json delete mode 100644 spec/requests/api/api_helpers_spec.rb create mode 100644 spec/requests/api/helpers_spec.rb create mode 100644 vendor/assets/javascripts/jquery.turbolinks.js diff --git a/CHANGELOG.md b/CHANGELOG.md index e11eb68240..2c49f44f84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,41 @@ Please view this file on the master branch, on stable branches it's out of date. +## 8.13.11 (2017-01-10) + +- Update the gitlab-markup gem to the version 1.5.1. !8509 +- Updated Turbolinks to mitigate potential XSS attacks. + +## 8.13.10 (2016-12-14) + +- API: Memoize the current_user so that sudo can work properly. !8017 +- Filter `authentication_token`, `incoming_email_token` and `runners_token` parameters. +- Issue#visible_to_user moved to IssuesFinder to prevent accidental use. +- Fix missing Note access checks by moving Note#search to updated NoteFinder. + +## 8.13.9 (2016-12-08) + +- Reenables /user API request to return private-token if user is admin and request is made with sudo. !7615 +- Replace MR access checks with use of MergeRequestsFinder. + +## 8.13.8 (2016-12-02) + +- Pass tag SHA to post-receive hook when tag is created via UI. !7700 +- Validate state param when filtering issuables. + +## 8.13.7 (2016-11-28) + +- fixes 500 error on project show when user is not logged in and project is still empty. !7376 +- Update grape entity to 0.6.0. !7491 +- Fix information disclosure in `Projects::BlobController#update`. +- Fix missing access checks on issue lookup using IssuableFinder. +- Replace issue access checks with use of IssuableFinder. +- Non members cannot create labels through the API. + ## 8.13.6 (2016-11-17) - Omniauth auto link LDAP user falls back to find by DN when user cannot be found by UID. !7002 +- Fix Milestone dropdown not stay selected for `Upcoming` and `No Milestone` option. !7117 +- Fix relative links in Markdown wiki when displayed in "Project" tab. !7218 - Fix no "Register" tab if ldap auth is enabled (#24038). !7274 (Luc Didry) - Fix cache for commit status in commits list to respect branches. !7372 - Fix issue causing Labels not to appear in sidebar on MR page. !7416 (Alex Sanford) diff --git a/Gemfile b/Gemfile index 5a95fdc82a..c5c51cfdff 100644 --- a/Gemfile +++ b/Gemfile @@ -68,7 +68,7 @@ gem 'github-linguist', '~> 4.7.0', require: 'linguist' # API gem 'grape', '~> 0.15.0' -gem 'grape-entity', '~> 0.4.2' +gem 'grape-entity', '~> 0.6.0' gem 'rack-cors', '~> 0.4.0', require: 'rack/cors' # Pagination @@ -101,7 +101,7 @@ gem 'seed-fu', '~> 2.3.5' # Markdown and HTML processing gem 'html-pipeline', '~> 1.11.0' gem 'deckar01-task_list', '1.0.5', require: 'task_list/railtie' -gem 'gitlab-markup', '~> 1.5.0' +gem 'gitlab-markup', '~> 1.5.1' gem 'redcarpet', '~> 3.3.3' gem 'RedCloth', '~> 4.3.2' gem 'rdoc', '~>3.6' @@ -214,8 +214,7 @@ gem 'chronic_duration', '~> 0.10.6' gem 'sass-rails', '~> 5.0.6' gem 'coffee-rails', '~> 4.1.0' gem 'uglifier', '~> 2.7.2' -gem 'turbolinks', '~> 2.5.0' -gem 'jquery-turbolinks', '~> 2.1.0' +gem 'gitlab-turbolinks-classic', '~> 2.5', '>= 2.5.6' gem 'addressable', '~> 2.3.8' gem 'bootstrap-sass', '~> 3.3.0' diff --git a/Gemfile.lock b/Gemfile.lock index 26c4dc0d23..0c052835df 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -282,7 +282,9 @@ GEM diff-lcs (~> 1.1) mime-types (>= 1.16, < 3) posix-spawn (~> 0.3) - gitlab-markup (1.5.0) + gitlab-markup (1.5.1) + gitlab-turbolinks-classic (2.5.6) + coffee-rails gitlab_git (10.7.0) activesupport (~> 4.0) charlock_holmes (~> 0.7.3) @@ -322,7 +324,7 @@ GEM rack-accept rack-mount virtus (>= 1.0.0) - grape-entity (0.4.8) + grape-entity (0.6.0) activesupport multi_json (>= 1.3.2) haml (4.0.7) @@ -361,9 +363,6 @@ GEM rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) - jquery-turbolinks (2.1.0) - railties (>= 3.1.0) - turbolinks jquery-ui-rails (5.0.5) railties (>= 3.2.16) json (1.8.3) @@ -751,8 +750,6 @@ GEM truncato (0.7.8) htmlentities (~> 4.3.1) nokogiri (~> 1.6.1) - turbolinks (2.5.3) - coffee-rails tzinfo (1.2.2) thread_safe (~> 0.1) u2f (0.2.1) @@ -866,14 +863,15 @@ DEPENDENCIES gemojione (~> 3.0) github-linguist (~> 4.7.0) gitlab-flowdock-git-hook (~> 1.0.1) - gitlab-markup (~> 1.5.0) + gitlab-markup (~> 1.5.1) + gitlab-turbolinks-classic (~> 2.5, >= 2.5.6) gitlab_git (~> 10.7.0) gitlab_omniauth-ldap (~> 1.2.1) gollum-lib (~> 4.2) gollum-rugged_adapter (~> 0.4.2) gon (~> 6.1.0) grape (~> 0.15.0) - grape-entity (~> 0.4.2) + grape-entity (~> 0.6.0) haml_lint (~> 0.18.2) hamlit (~> 2.6.1) health_check (~> 2.2.0) @@ -883,7 +881,6 @@ DEPENDENCIES influxdb (~> 0.2) jquery-atwho-rails (~> 1.3.2) jquery-rails (~> 4.1.0) - jquery-turbolinks (~> 2.1.0) jquery-ui-rails (~> 5.0.0) json-schema (~> 2.6.2) jwt @@ -979,7 +976,6 @@ DEPENDENCIES thin (~> 1.7.0) timecop (~> 0.8.0) truncato (~> 0.7.8) - turbolinks (~> 2.5.0) u2f (~> 0.2.1) uglifier (~> 2.7.2) underscore-rails (~> 1.8.0) @@ -994,4 +990,4 @@ DEPENDENCIES wikicloth (= 0.8.1) BUNDLED WITH - 1.13.5 + 1.13.6 diff --git a/VERSION b/VERSION index 6b63235a86..3b08e80e2a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.13.6 +8.13.11 diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb index dacb5679dd..936d9bab57 100644 --- a/app/controllers/concerns/creates_commit.rb +++ b/app/controllers/concerns/creates_commit.rb @@ -81,10 +81,8 @@ module CreatesCommit def merge_request_exists? return @merge_request if defined?(@merge_request) - @merge_request = @mr_target_project.merge_requests.opened.find_by( - source_branch: @mr_source_branch, - target_branch: @mr_target_branch - ) + @merge_request = MergeRequestsFinder.new(current_user, project_id: @mr_target_project.id).execute.opened. + find_by(source_branch: @mr_source_branch, target_branch: @mr_target_branch) end def different_project? diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index b78cc6585b..398122b307 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -13,7 +13,6 @@ class Projects::BlobController < Projects::ApplicationController before_action :assign_blob_vars before_action :commit, except: [:new, :create] before_action :blob, except: [:new, :create] - before_action :from_merge_request, only: [:edit, :update] before_action :require_branch_head, only: [:edit, :update] before_action :editor_variables, except: [:show, :preview, :diff] before_action :validate_diff_params, only: :diff @@ -39,14 +38,6 @@ class Projects::BlobController < Projects::ApplicationController def update @path = params[:file_path] if params[:file_path].present? - after_edit_path = - if from_merge_request && @target_branch == @ref - diffs_namespace_project_merge_request_path(from_merge_request.target_project.namespace, from_merge_request.target_project, from_merge_request) + - "#file-path-#{hexdigest(@path)}" - else - namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @path)) - end - create_commit(Files::UpdateService, success_path: after_edit_path, failure_view: :edit, failure_path: namespace_project_blob_path(@project.namespace, @project, @id)) @@ -124,9 +115,14 @@ class Projects::BlobController < Projects::ApplicationController render_404 end - def from_merge_request - # If blob edit was initiated from merge request page - @from_merge_request ||= MergeRequest.find_by(id: params[:from_merge_request_id]) + def after_edit_path + from_merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:from_merge_request_iid]) + if from_merge_request && @target_branch == @ref + diffs_namespace_project_merge_request_path(from_merge_request.target_project.namespace, from_merge_request.target_project, from_merge_request) + + "#file-path-#{hexdigest(@path)}" + else + namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @path)) + end end def editor_variables diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index 2de8ada3e2..56c939b5cc 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -36,7 +36,7 @@ class Projects::BranchesController < Projects::ApplicationController execute(branch_name, ref) if params[:issue_iid] - issue = @project.issues.find_by(iid: params[:issue_iid]) + issue = IssuesFinder.new(current_user, project_id: @project.id).find_by(iid: params[:issue_iid]) SystemNoteService.new_issue_branch(issue, @project, current_user, branch_name) if issue end diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index cdfc1ba7b9..8197d9e4c9 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -65,7 +65,7 @@ class Projects::CommitController < Projects::ApplicationController return render_404 if @target_branch.blank? - create_commit(Commits::RevertService, success_notice: "The #{@commit.change_type_title} has been successfully reverted.", + create_commit(Commits::RevertService, success_notice: "The #{@commit.change_type_title(current_user)} has been successfully reverted.", success_path: successful_change_path, failure_path: failed_change_path) end @@ -74,26 +74,24 @@ class Projects::CommitController < Projects::ApplicationController return render_404 if @target_branch.blank? - create_commit(Commits::CherryPickService, success_notice: "The #{@commit.change_type_title} has been successfully cherry-picked.", + create_commit(Commits::CherryPickService, success_notice: "The #{@commit.change_type_title(current_user)} has been successfully cherry-picked.", success_path: successful_change_path, failure_path: failed_change_path) end private def successful_change_path - return referenced_merge_request_url if @commit.merged_merge_request - - namespace_project_commits_url(@project.namespace, @project, @target_branch) + referenced_merge_request_url || namespace_project_commits_url(@project.namespace, @project, @target_branch) end def failed_change_path - return referenced_merge_request_url if @commit.merged_merge_request - - namespace_project_commit_url(@project.namespace, @project, params[:id]) + referenced_merge_request_url || namespace_project_commit_url(@project.namespace, @project, params[:id]) end def referenced_merge_request_url - namespace_project_merge_request_url(@project.namespace, @project, @commit.merged_merge_request) + if merge_request = @commit.merged_merge_request(current_user) + namespace_project_merge_request_url(@project.namespace, @project, merge_request) + end end def commit diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb index 4f38b2202f..8db8e5b4c7 100644 --- a/app/controllers/projects/commits_controller.rb +++ b/app/controllers/projects/commits_controller.rb @@ -21,7 +21,7 @@ class Projects::CommitsController < Projects::ApplicationController @note_counts = project.notes.where(commit_id: @commits.map(&:id)). group(:commit_id).count - @merge_request = @project.merge_requests.opened. + @merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened. find_by(source_project: @project, source_branch: @ref, target_branch: @repository.root_ref) respond_to do |format| diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index bee3d56076..ec02fc15d3 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -53,7 +53,7 @@ class Projects::CompareController < Projects::ApplicationController end def merge_request - @merge_request ||= @project.merge_requests.opened. + @merge_request ||= MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened. find_by(source_project: @project, source_branch: @head_ref, target_branch: @start_ref) end end diff --git a/app/controllers/projects/cycle_analytics_controller.rb b/app/controllers/projects/cycle_analytics_controller.rb index 16a7b1fc6e..699982de9d 100644 --- a/app/controllers/projects/cycle_analytics_controller.rb +++ b/app/controllers/projects/cycle_analytics_controller.rb @@ -5,7 +5,7 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController before_action :authorize_read_cycle_analytics! def show - @cycle_analytics = CycleAnalytics.new(@project, from: parse_start_date) + @cycle_analytics = CycleAnalytics.new(@project, current_user, from: parse_start_date) respond_to do |format| format.html diff --git a/app/controllers/projects/discussions_controller.rb b/app/controllers/projects/discussions_controller.rb index d174e1145a..9302a68b83 100644 --- a/app/controllers/projects/discussions_controller.rb +++ b/app/controllers/projects/discussions_controller.rb @@ -26,7 +26,7 @@ class Projects::DiscussionsController < Projects::ApplicationController private def merge_request - @merge_request ||= @project.merge_requests.find_by!(iid: params[:merge_request_id]) + @merge_request ||= MergeRequestsFinder.new(current_user, project_id: @project.id).find_by!(iid: params[:merge_request_id]) end def discussion diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index 0948ad2164..709710d785 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -215,6 +215,6 @@ class Projects::NotesController < Projects::ApplicationController end def find_current_user_notes - @notes = NotesFinder.new.execute(project, current_user, params) + @notes = NotesFinder.new(project, current_user, params).execute.inc_author end end diff --git a/app/controllers/projects/todos_controller.rb b/app/controllers/projects/todos_controller.rb index 5685d0f4e7..a41fcb85c4 100644 --- a/app/controllers/projects/todos_controller.rb +++ b/app/controllers/projects/todos_controller.rb @@ -16,15 +16,9 @@ class Projects::TodosController < Projects::ApplicationController @issuable ||= begin case params[:issuable_type] when "issue" - issue = @project.issues.find(params[:issuable_id]) - - if can?(current_user, :read_issue, issue) - issue - else - render_404 - end + IssuesFinder.new(current_user, project_id: @project.id).find(params[:issuable_id]) when "merge_request" - @project.merge_requests.find(params[:issuable_id]) + MergeRequestsFinder.new(current_user, project_id: @project.id).find(params[:issuable_id]) end end end diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 6297b2db36..22bc29861a 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -7,7 +7,7 @@ # current_user - which user use # params: # scope: 'created-by-me' or 'assigned-to-me' or 'all' -# state: 'open' or 'closed' or 'all' +# state: 'opened' or 'closed' or 'all' # group_id: integer # project_id: integer # milestone_title: string @@ -23,7 +23,7 @@ class IssuableFinder attr_accessor :current_user, :params - def initialize(current_user, params) + def initialize(current_user, params = {}) @current_user = current_user @params = params end @@ -43,6 +43,18 @@ class IssuableFinder sort(items) end + def find(*params) + execute.find(*params) + end + + def find_by(*params) + execute.find_by(*params) + end + + def find_by!(*params) + execute.find_by!(*params) + end + def group return @group if defined?(@group) @@ -175,10 +187,13 @@ class IssuableFinder end def by_state(items) - params[:state] ||= 'all' - - if items.respond_to?(params[:state]) - items.public_send(params[:state]) + case params[:state].to_s + when 'closed' + items.closed + when 'merged' + items.respond_to?(:merged) ? items.merged : items.closed + when 'opened' + items.opened else items end diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index be00a21920..707eddd4d2 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -23,10 +23,26 @@ class IssuesFinder < IssuableFinder private def init_collection - Issue.visible_to_user(current_user) + IssuesFinder.not_restricted_by_confidentiality(current_user) end def iid_pattern @iid_pattern ||= %r{\A#{Regexp.escape(Issue.reference_prefix)}(?\d+)\z} end + + def self.not_restricted_by_confidentiality(user) + return Issue.where('issues.confidential IS NULL OR issues.confidential IS FALSE') if user.blank? + + return Issue.all if user.admin? + + Issue.where(' + issues.confidential IS NULL + OR issues.confidential IS FALSE + OR (issues.confidential = TRUE + AND (issues.author_id = :user_id + OR issues.assignee_id = :user_id + OR issues.project_id IN(:project_ids)))', + user_id: user.id, + project_ids: user.authorized_projects(Gitlab::Access::REPORTER).select(:id)) + end end diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb index 0b7832e658..4bd8c83081 100644 --- a/app/finders/notes_finder.rb +++ b/app/finders/notes_finder.rb @@ -1,27 +1,102 @@ class NotesFinder FETCH_OVERLAP = 5.seconds - def execute(project, current_user, params) - target_type = params[:target_type] - target_id = params[:target_id] - # Default to 0 to remain compatible with old clients - last_fetched_at = Time.at(params.fetch(:last_fetched_at, 0).to_i) + # Used to filter Notes + # When used with target_type and target_id this returns notes specifically for the controller + # + # Arguments: + # current_user - which user check authorizations with + # project - which project to look for notes on + # params: + # target_type: string + # target_id: integer + # last_fetched_at: time + # search: string + # + def initialize(project, current_user, params = {}) + @project = project + @current_user = current_user + @params = params + init_collection + end - notes = - case target_type - when "commit" - project.notes.for_commit_id(target_id).non_diff_notes - when "issue" - project.issues.visible_to_user(current_user).find(target_id).notes.inc_author - when "merge_request" - project.merge_requests.find(target_id).mr_and_commit_notes.inc_author - when "snippet", "project_snippet" - project.snippets.find(target_id).notes + def execute + @notes = since_fetch_at(@params[:last_fetched_at]) if @params[:last_fetched_at] + @notes + end + + private + + def init_collection + if @params[:target_id] + @notes = on_target(@params[:target_type], @params[:target_id]) + else + @notes = notes_of_any_type + end + end + + def notes_of_any_type + types = %w(commit issue merge_request snippet) + note_relations = types.map { |t| notes_for_type(t) } + note_relations.map!{ |notes| search(@params[:search], notes) } if @params[:search] + UnionFinder.new.find_union(note_relations, Note) + end + + def noteables_for_type(noteable_type) + case noteable_type + when "issue" + IssuesFinder.new(@current_user, project_id: @project.id).execute + when "merge_request" + MergeRequestsFinder.new(@current_user, project_id: @project.id).execute + when "snippet", "project_snippet" + SnippetsFinder.new.execute(@current_user, filter: :by_project, project: @project) + else + raise 'invalid target_type' + end + end + + def notes_for_type(noteable_type) + if noteable_type == "commit" + if Ability.allowed?(@current_user, :download_code, @project) + @project.notes.where(noteable_type: 'Commit') else - raise 'invalid target_type' + Note.none end + else + finder = noteables_for_type(noteable_type) + @project.notes.where(noteable_type: finder.base_class.name, noteable_id: finder.reorder(nil)) + end + end - # Use overlapping intervals to avoid worrying about race conditions - notes.where('updated_at > ?', last_fetched_at - FETCH_OVERLAP).fresh + def on_target(target_type, target_id) + if target_type == "commit" + notes_for_type('commit').for_commit_id(target_id) + else + target = noteables_for_type(target_type).find(target_id) + + if target.respond_to?(:related_notes) + target.related_notes + else + target.notes + end + end + end + + # Searches for notes matching the given query. + # + # This method uses ILIKE on PostgreSQL and LIKE on MySQL. + # + def search(query, notes_relation = @notes) + pattern = "%#{query}%" + notes_relation.where(Note.arel_table[:note].matches(pattern)) + end + + # Notes changed since last fetch + # Uses overlapping intervals to avoid worrying about race conditions + def since_fetch_at(fetch_time) + # Default to 0 to remain compatible with old clients + last_fetched_at = Time.at(@params.fetch(:last_fetched_at, 0).to_i) + + @notes.where('updated_at > ?', last_fetched_at - FETCH_OVERLAP).fresh end end diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index ed402b698f..66a720a942 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -130,7 +130,7 @@ module CommitsHelper def revert_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true) return unless current_user - tooltip = "Revert this #{commit.change_type_title} in a new merge request" if has_tooltip + tooltip = "Revert this #{commit.change_type_title(current_user)} in a new merge request" if has_tooltip if can_collaborate_with_project? btn_class = "btn btn-warning btn-#{btn_class}" unless btn_class.nil? @@ -154,7 +154,7 @@ module CommitsHelper def cherry_pick_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true) return unless current_user - tooltip = "Cherry-pick this #{commit.change_type_title} in a new merge request" + tooltip = "Cherry-pick this #{commit.change_type_title(current_user)} in a new merge request" if can_collaborate_with_project? btn_class = "btn btn-default btn-#{btn_class}" unless btn_class.nil? diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb index a46f2c6e17..6e68aad4cb 100644 --- a/app/helpers/preferences_helper.rb +++ b/app/helpers/preferences_helper.rb @@ -50,7 +50,7 @@ module PreferencesHelper end def default_project_view - return 'readme' unless current_user + return anonymous_project_view unless current_user user_view = current_user.project_view @@ -66,4 +66,8 @@ module PreferencesHelper "customize_workflow" end end + + def anonymous_project_view + @project.empty_repo? || !can?(current_user, :download_code, @project) ? 'activity' : 'readme' + end end diff --git a/app/models/commit.rb b/app/models/commit.rb index 9e7fde9503..fe27866164 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -249,44 +249,47 @@ class Commit project.repository.next_branch("cherry-pick-#{short_id}", mild: true) end - def revert_description - if merged_merge_request - "This reverts merge request #{merged_merge_request.to_reference}" + def revert_description(user) + if merged_merge_request?(user) + "This reverts merge request #{merged_merge_request(user).to_reference}" else "This reverts commit #{sha}" end end - def revert_message - %Q{Revert "#{title.strip}"\n\n#{revert_description}} + def revert_message(user) + %Q{Revert "#{title.strip}"\n\n#{revert_description(user)}} end - def reverts_commit?(commit) - description? && description.include?(commit.revert_description) + def reverts_commit?(commit, user) + description? && description.include?(commit.revert_description(user)) end def merge_commit? parents.size > 1 end - def merged_merge_request - return @merged_merge_request if defined?(@merged_merge_request) - - @merged_merge_request = project.merge_requests.find_by(merge_commit_sha: id) if merge_commit? + def merged_merge_request(current_user) + # Memoize with per-user access check + @merged_merge_request_hash ||= Hash.new do |hash, user| + hash[user] = merged_merge_request_no_cache(user) + end + + @merged_merge_request_hash[current_user] end - def has_been_reverted?(current_user = nil, noteable = self) + def has_been_reverted?(current_user, noteable = self) ext = all_references(current_user) noteable.notes_with_associations.system.each do |note| note.all_references(current_user, extractor: ext) end - ext.commits.any? { |commit_ref| commit_ref.reverts_commit?(self) } + ext.commits.any? { |commit_ref| commit_ref.reverts_commit?(self, current_user) } end - def change_type_title - merged_merge_request ? 'merge request' : 'commit' + def change_type_title(user) + merged_merge_request?(user) ? 'merge request' : 'commit' end # Get the URI type of the given path @@ -344,4 +347,12 @@ class Commit changes end + + def merged_merge_request?(user) + !!merged_merge_request(user) + end + + def merged_merge_request_no_cache(user) + MergeRequestsFinder.new(user, project_id: project.id).find_by(merge_commit_sha: id) if merge_commit? + end end diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb index 7bcc78247b..99f17d668e 100644 --- a/app/models/concerns/milestoneish.rb +++ b/app/models/concerns/milestoneish.rb @@ -1,17 +1,17 @@ module Milestoneish - def closed_items_count(user = nil) + def closed_items_count(user) issues_visible_to_user(user).closed.size + merge_requests.closed_and_merged.size end - def total_items_count(user = nil) + def total_items_count(user) issues_visible_to_user(user).size + merge_requests.size end - def complete?(user = nil) + def complete?(user) total_items_count(user) > 0 && total_items_count(user) == closed_items_count(user) end - def percent_complete(user = nil) + def percent_complete(user) ((closed_items_count(user) * 100) / total_items_count(user)).abs rescue ZeroDivisionError 0 @@ -23,7 +23,7 @@ module Milestoneish (due_date - Date.today).to_i end - def issues_visible_to_user(user = nil) - issues.visible_to_user(user) + def issues_visible_to_user(user) + IssuesFinder.new(user).execute.where(id: issues) end end diff --git a/app/models/cycle_analytics.rb b/app/models/cycle_analytics.rb index 8ed4a56b19..c4c7d670a4 100644 --- a/app/models/cycle_analytics.rb +++ b/app/models/cycle_analytics.rb @@ -4,13 +4,14 @@ class CycleAnalytics DEPLOYMENT_METRIC_STAGES = %i[production staging] - def initialize(project, from:) + def initialize(project, current_user, from:) @project = project + @current_user = current_user @from = from end def summary - @summary ||= Summary.new(@project, from: @from) + @summary ||= Summary.new(@project, @current_user, from: @from) end def issue diff --git a/app/models/cycle_analytics/summary.rb b/app/models/cycle_analytics/summary.rb index b46db449bf..82f53d17dd 100644 --- a/app/models/cycle_analytics/summary.rb +++ b/app/models/cycle_analytics/summary.rb @@ -1,12 +1,13 @@ class CycleAnalytics class Summary - def initialize(project, from:) + def initialize(project, current_user, from:) @project = project + @current_user = current_user @from = from end def new_issues - @project.issues.created_after(@from).count + IssuesFinder.new(@current_user, project_id: @project.id).execute.created_after(@from).count end def commits diff --git a/app/models/issue.rb b/app/models/issue.rb index f0fe32187e..9b581aab84 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -61,61 +61,6 @@ class Issue < ActiveRecord::Base attributes end - class << self - private - - # Returns the project that the current scope belongs to if any, nil otherwise. - # - # Examples: - # - my_project.issues.without_due_date.owner_project => my_project - # - Issue.all.owner_project => nil - def owner_project - # No owner if we're not being called from an association - return unless all.respond_to?(:proxy_association) - - owner = all.proxy_association.owner - - # Check if the association is or belongs to a project - if owner.is_a?(Project) - owner - else - begin - owner.association(:project).target - rescue ActiveRecord::AssociationNotFoundError - nil - end - end - end - end - - def self.visible_to_user(user) - return where('issues.confidential IS NULL OR issues.confidential IS FALSE') if user.blank? - return all if user.admin? - - # Check if we are scoped to a specific project's issues - if owner_project - if owner_project.authorized_for_user?(user, Gitlab::Access::REPORTER) - # If the project is authorized for the user, they can see all issues in the project - return all - else - # else only non confidential and authored/assigned to them - return where('issues.confidential IS NULL OR issues.confidential IS FALSE - OR issues.author_id = :user_id OR issues.assignee_id = :user_id', - user_id: user.id) - end - end - - where(' - issues.confidential IS NULL - OR issues.confidential IS FALSE - OR (issues.confidential = TRUE - AND (issues.author_id = :user_id - OR issues.assignee_id = :user_id - OR issues.project_id IN(:project_ids)))', - user_id: user.id, - project_ids: user.authorized_projects(Gitlab::Access::REPORTER).select(:id)) - end - def self.reference_prefix '#' end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 7663e055f0..a954c7d670 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -453,7 +453,7 @@ class MergeRequest < ActiveRecord::Base should_remove_source_branch? || force_remove_source_branch? end - def mr_and_commit_notes + def related_notes # Fetch comments only from last 100 commits commits_for_notes_limit = 100 commit_ids = commits.last(commits_for_notes_limit).map(&:id) @@ -469,7 +469,7 @@ class MergeRequest < ActiveRecord::Base end def discussions - @discussions ||= self.mr_and_commit_notes. + @discussions ||= self.related_notes. inc_relations_for_view. fresh. discussions @@ -803,7 +803,7 @@ class MergeRequest < ActiveRecord::Base @merge_commit ||= project.commit(merge_commit_sha) if merge_commit_sha end - def can_be_reverted?(current_user = nil) + def can_be_reverted?(current_user) merge_commit && !merge_commit.has_been_reverted?(current_user, self) end diff --git a/app/models/note.rb b/app/models/note.rb index 2d644b03e4..f4730bc989 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -103,23 +103,6 @@ class Note < ActiveRecord::Base Discussion.for_diff_notes(active_notes). map { |d| [d.line_code, d] }.to_h end - - # Searches for notes matching the given query. - # - # This method uses ILIKE on PostgreSQL and LIKE on MySQL. - # - # query - The search query as a String. - # as_user - Limit results to those viewable by a specific user - # - # Returns an ActiveRecord::Relation. - def search(query, as_user: nil) - table = arel_table - pattern = "%#{query}%" - - Note.joins('LEFT JOIN issues ON issues.id = noteable_id'). - where(table[:note].matches(pattern)). - merge(Issue.visible_to_user(as_user)) - end end def cross_reference? diff --git a/app/models/project.rb b/app/models/project.rb index e0062f3dbb..3f291d3944 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -679,9 +679,9 @@ class Project < ActiveRecord::Base self.id end - def get_issue(issue_id) + def get_issue(issue_id, current_user) if default_issues_tracker? - issues.find_by(iid: issue_id) + IssuesFinder.new(current_user, project_id: id).find_by(iid: issue_id) else ExternalIssue.new(issue_id, self) end diff --git a/app/models/repository.rb b/app/models/repository.rb index fe4b73e9aa..2639cbd108 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -167,8 +167,9 @@ class Repository options = { message: message, tagger: user_to_committer(user) } if message - GitHooksService.new.execute(user, path_to_repo, oldrev, target, ref) do - rugged.tags.create(tag_name, target, options) + GitHooksService.new.execute(user, path_to_repo, oldrev, target, ref) do |service| + raw_tag = rugged.tags.create(tag_name, target, options) + service.newrev = raw_tag.target_id end find_tag(tag_name) @@ -953,7 +954,7 @@ class Repository update_branch_with_hooks(user, base_branch) do committer = user_to_committer(user) source_sha = Rugged::Commit.create(rugged, - message: commit.revert_message, + message: commit.revert_message(user), author: committer, committer: committer, tree: revert_tree_id, diff --git a/app/models/user.rb b/app/models/user.rb index 9181db40eb..40307d0ca9 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -275,10 +275,6 @@ class User < ActiveRecord::Base personal_access_token.user if personal_access_token end - def by_username_or_id(name_or_id) - find_by('users.username = ? OR users.id = ?', name_or_id.to_s, name_or_id.to_i) - end - # Returns a user for the given SSH key. def find_by_ssh_key_id(key_id) find_by(id: Key.unscoped.select(:user_id).where(id: key_id)) diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb index 1c82599c57..db5f2bf9b2 100644 --- a/app/services/commits/change_service.rb +++ b/app/services/commits/change_service.rb @@ -34,7 +34,7 @@ module Commits repository.public_send(action, current_user, @commit, into, tree_id) success else - error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title} automatically. + error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title(current_user)} automatically. It may have already been #{action.to_s.dasherize}, or a more recent commit may have updated some of its content." raise ChangeError, error_msg end diff --git a/app/services/git_hooks_service.rb b/app/services/git_hooks_service.rb index 172bd85dad..6cd3908d43 100644 --- a/app/services/git_hooks_service.rb +++ b/app/services/git_hooks_service.rb @@ -1,6 +1,8 @@ class GitHooksService PreReceiveError = Class.new(StandardError) + attr_accessor :oldrev, :newrev, :ref + def execute(user, repo_path, oldrev, newrev, ref) @repo_path = repo_path @user = Gitlab::GlId.gl_id(user) @@ -16,7 +18,7 @@ class GitHooksService end end - yield + yield self run_hook('post-receive') end @@ -25,6 +27,6 @@ class GitHooksService def run_hook(name) hook = Gitlab::Git::Hook.new(name, @repo_path) - hook.trigger(@user, @oldrev, @newrev, @ref) + hook.trigger(@user, oldrev, newrev, ref) end end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index bb92cd80cc..0511d1dbf3 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -85,14 +85,15 @@ class IssuableBaseService < BaseService def find_or_create_label_ids labels = params.delete(:labels) + return unless labels - params[:label_ids] = labels.split(',').map do |label_name| + params[:label_ids] = labels.split(",").map do |label_name| service = Labels::FindOrCreateService.new(current_user, project, title: label_name.strip) label = service.execute - label.id - end + label.try(:id) + end.compact end def process_label_ids(attributes, existing_label_ids: nil) @@ -140,6 +141,7 @@ class IssuableBaseService < BaseService params.delete(:state_event) params[:author] ||= current_user + label_ids = process_label_ids(params) issuable.assign_attributes(params) diff --git a/app/services/labels/find_or_create_service.rb b/app/services/labels/find_or_create_service.rb index d622f9edd3..cf4f7606c9 100644 --- a/app/services/labels/find_or_create_service.rb +++ b/app/services/labels/find_or_create_service.rb @@ -22,9 +22,14 @@ module Labels ).execute(skip_authorization: skip_authorization) end + # Only creates the label if current_user can do so, if the label does not exist + # and the user can not create the label, nil is returned def find_or_create_label new_label = available_labels.find_by(title: title) - new_label ||= project.labels.create(params) + + if new_label.nil? && (skip_authorization || Ability.allowed?(current_user, :admin_label, project)) + new_label = project.labels.create(params) + end new_label end diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index 404f75616b..f24346428e 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -65,7 +65,7 @@ module MergeRequests commit = commits.first merge_request.title = commit.title merge_request.description ||= commit.description.try(:strip) - elsif iid && (issue = merge_request.target_project.get_issue(iid)) && !issue.try(:confidential?) + elsif iid && issue = merge_request.target_project.get_issue(iid, current_user) case issue when Issue merge_request.title = "Resolve \"#{issue.title}\"" diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 99a58bbb67..7bd11f5727 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -70,14 +70,14 @@ %span Issues - if @project.default_issues_tracker? - %span.badge.count.issue_counter= number_with_delimiter(@project.issues.visible_to_user(current_user).opened.count) + %span.badge.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id).execute.opened.count) - if project_nav_tab? :merge_requests = nav_link(controller: :merge_requests) do = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do %span Merge Requests - %span.badge.count.merge_counter= number_with_delimiter(@project.merge_requests.opened.count) + %span.badge.count.merge_counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count) - if project_nav_tab? :wiki = nav_link(controller: :wikis) do diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml index 2a0352a71b..a5dcd93f42 100644 --- a/app/views/projects/blob/edit.html.haml +++ b/app/views/projects/blob/edit.html.haml @@ -27,5 +27,5 @@ = render 'shared/new_commit_form', placeholder: "Update #{@blob.name}" = hidden_field_tag 'last_commit_sha', @last_commit_sha = hidden_field_tag 'content', '', id: "file-content" - = hidden_field_tag 'from_merge_request_id', params[:from_merge_request_id] + = hidden_field_tag 'from_merge_request_iid', params[:from_merge_request_iid] = render 'projects/commit_button', ref: @ref, cancel_path: namespace_project_blob_path(@project.namespace, @project, @id) diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml index e4cd55b9f7..f6e3d5e76f 100644 --- a/app/views/projects/commit/_change.html.haml +++ b/app/views/projects/commit/_change.html.haml @@ -11,7 +11,7 @@ .modal-content .modal-header %a.close{href: "#", "data-dismiss" => "modal"} × - %h3.page-title== #{label} this #{commit.change_type_title} + %h3.page-title== #{label} this #{commit.change_type_title(current_user)} .modal-body = form_tag send("#{type.underscore}_namespace_project_commit_path", @project.namespace, @project, commit.id), method: :post, remote: false, class: 'form-horizontal js-#{type}-form js-requires-input' do .form-group.branch diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index 257e0a855b..2254bc92f3 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -10,7 +10,7 @@ \ = clipboard_button(clipboard_text: diff_file.new_path, class: 'btn-file-option') - if editable_diff?(diff_file) - - link_opts = @merge_request.id ? { from_merge_request_id: @merge_request.id } : {} + - link_opts = @merge_request.persisted? ? { from_merge_request_iid: @merge_request.iid } : {} = edit_blob_link(@merge_request.source_project, @merge_request.source_branch, diff_file.new_path, blob: blob, link_opts: link_opts) diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index 1240806883..8e4be6018b 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -39,7 +39,7 @@ = icon('thumbs-down') = downvotes - - note_count = merge_request.mr_and_commit_notes.user.count + - note_count = merge_request.related_notes.user.count %li = link_to merge_request_path(merge_request, anchor: 'notes'), class: ('no-comments' if note_count.zero?) do = icon('comments') diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index f57abe7397..453c48872c 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -53,7 +53,7 @@ %li.notes-tab = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#notes', action: 'notes', toggle: 'tab' } do Discussion - %span.badge= @merge_request.mr_and_commit_notes.user.count + %span.badge= @merge_request.related_notes.user.count - if @merge_request.source_project %li.commits-tab = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#commits', action: 'commits', toggle: 'tab' } do diff --git a/config/application.rb b/config/application.rb index 92c8467e7f..b8ca6a509c 100644 --- a/config/application.rb +++ b/config/application.rb @@ -45,7 +45,7 @@ module Gitlab # # Parameters filtered: # - Password (:password, :password_confirmation) - # - Private tokens (:private_token) + # - Private tokens # - Two-factor tokens (:otp_attempt) # - Repo/Project Import URLs (:import_url) # - Build variables (:variables) @@ -55,15 +55,18 @@ module Gitlab # - Sentry DSN (:sentry_dsn) # - Deploy keys (:key) config.filter_parameters += %i( + authentication_token certificate encrypted_key hook import_url + incoming_email_token key otp_attempt password password_confirmation private_token + runners_token secret_token sentry_dsn variables diff --git a/db/migrate/20130319214458_create_forked_project_links.rb b/db/migrate/20130319214458_create_forked_project_links.rb index 66eb11a4b2..38b1150919 100644 --- a/db/migrate/20130319214458_create_forked_project_links.rb +++ b/db/migrate/20130319214458_create_forked_project_links.rb @@ -1,5 +1,7 @@ # rubocop:disable all class CreateForkedProjectLinks < ActiveRecord::Migration + DOWNTIME = false + def change create_table :forked_project_links do |t| t.integer :forked_to_project_id, null: false diff --git a/db/migrate/20130506090604_create_deploy_keys_projects.rb b/db/migrate/20130506090604_create_deploy_keys_projects.rb index 7d6662d358..c150a747b3 100644 --- a/db/migrate/20130506090604_create_deploy_keys_projects.rb +++ b/db/migrate/20130506090604_create_deploy_keys_projects.rb @@ -1,5 +1,7 @@ # rubocop:disable all class CreateDeployKeysProjects < ActiveRecord::Migration + DOWNTIME = false + def change create_table :deploy_keys_projects do |t| t.integer :deploy_key_id, null: false diff --git a/db/migrate/20130617095603_create_users_groups.rb b/db/migrate/20130617095603_create_users_groups.rb index 45cff93fe4..dc71f43890 100644 --- a/db/migrate/20130617095603_create_users_groups.rb +++ b/db/migrate/20130617095603_create_users_groups.rb @@ -1,5 +1,7 @@ # rubocop:disable all class CreateUsersGroups < ActiveRecord::Migration + DOWNTIME = false + def change create_table :users_groups do |t| t.integer :group_access, null: false diff --git a/db/migrate/20130711063759_create_project_group_links.rb b/db/migrate/20130711063759_create_project_group_links.rb index bd9d40a50d..651b597f5c 100644 --- a/db/migrate/20130711063759_create_project_group_links.rb +++ b/db/migrate/20130711063759_create_project_group_links.rb @@ -1,5 +1,7 @@ # rubocop:disable all class CreateProjectGroupLinks < ActiveRecord::Migration + DOWNTIME = false + def change create_table :project_group_links do |t| t.integer :project_id, null: false diff --git a/db/migrate/20131112114325_create_broadcast_messages.rb b/db/migrate/20131112114325_create_broadcast_messages.rb index ce37a8e270..06ea9535b3 100644 --- a/db/migrate/20131112114325_create_broadcast_messages.rb +++ b/db/migrate/20131112114325_create_broadcast_messages.rb @@ -1,5 +1,7 @@ # rubocop:disable all class CreateBroadcastMessages < ActiveRecord::Migration + DOWNTIME = false + def change create_table :broadcast_messages do |t| t.text :message, null: false diff --git a/db/migrate/20140122112253_create_merge_request_diffs.rb b/db/migrate/20140122112253_create_merge_request_diffs.rb index 395c3edfc7..01b9fcbcbf 100644 --- a/db/migrate/20140122112253_create_merge_request_diffs.rb +++ b/db/migrate/20140122112253_create_merge_request_diffs.rb @@ -1,5 +1,7 @@ # rubocop:disable all class CreateMergeRequestDiffs < ActiveRecord::Migration + DOWNTIME = false + def up create_table :merge_request_diffs do |t| t.string :state, null: false, default: 'collected' diff --git a/db/migrate/20140209025651_create_emails.rb b/db/migrate/20140209025651_create_emails.rb index 571beb19cd..97f53d96eb 100644 --- a/db/migrate/20140209025651_create_emails.rb +++ b/db/migrate/20140209025651_create_emails.rb @@ -1,10 +1,12 @@ # rubocop:disable all class CreateEmails < ActiveRecord::Migration + DOWNTIME = false + def change create_table :emails do |t| t.integer :user_id, null: false t.string :email, null: false - + t.timestamps end diff --git a/db/migrate/20140625115202_create_users_star_projects.rb b/db/migrate/20140625115202_create_users_star_projects.rb index 32dd99e83b..f5928823da 100644 --- a/db/migrate/20140625115202_create_users_star_projects.rb +++ b/db/migrate/20140625115202_create_users_star_projects.rb @@ -1,5 +1,7 @@ # rubocop:disable all class CreateUsersStarProjects < ActiveRecord::Migration + DOWNTIME = false + def change create_table :users_star_projects do |t| t.integer :project_id, null: false diff --git a/db/migrate/20140729134820_create_labels.rb b/db/migrate/20140729134820_create_labels.rb index df0f8cb9f0..75705a0f3e 100644 --- a/db/migrate/20140729134820_create_labels.rb +++ b/db/migrate/20140729134820_create_labels.rb @@ -1,5 +1,7 @@ # rubocop:disable all class CreateLabels < ActiveRecord::Migration + DOWNTIME = false + def change create_table :labels do |t| t.string :title diff --git a/db/migrate/20140729140420_create_label_links.rb b/db/migrate/20140729140420_create_label_links.rb index fa5992605f..7c9ff7d6a2 100644 --- a/db/migrate/20140729140420_create_label_links.rb +++ b/db/migrate/20140729140420_create_label_links.rb @@ -1,5 +1,7 @@ # rubocop:disable all class CreateLabelLinks < ActiveRecord::Migration + DOWNTIME = false + def change create_table :label_links do |t| t.integer :label_id diff --git a/db/migrate/20140914113604_add_members_table.rb b/db/migrate/20140914113604_add_members_table.rb index bc3c1bb61e..325b745762 100644 --- a/db/migrate/20140914113604_add_members_table.rb +++ b/db/migrate/20140914113604_add_members_table.rb @@ -1,5 +1,7 @@ # rubocop:disable all class AddMembersTable < ActiveRecord::Migration + DOWNTIME = false + def change create_table :members do |t| t.integer :access_level, null: false diff --git a/db/migrate/20140914173417_remove_old_member_tables.rb b/db/migrate/20140914173417_remove_old_member_tables.rb index aff8e94e5b..0ab4c7b1cd 100644 --- a/db/migrate/20140914173417_remove_old_member_tables.rb +++ b/db/migrate/20140914173417_remove_old_member_tables.rb @@ -1,5 +1,7 @@ # rubocop:disable all class RemoveOldMemberTables < ActiveRecord::Migration + DOWNTIME = false + def up drop_table :users_groups drop_table :users_projects diff --git a/db/migrate/20141118150935_add_audit_event.rb b/db/migrate/20141118150935_add_audit_event.rb index 3884228456..03096e7c6d 100644 --- a/db/migrate/20141118150935_add_audit_event.rb +++ b/db/migrate/20141118150935_add_audit_event.rb @@ -1,5 +1,7 @@ # rubocop:disable all class AddAuditEvent < ActiveRecord::Migration + DOWNTIME = false + def change create_table :audit_events do |t| t.integer :author_id, null: false diff --git a/db/migrate/20141216155758_create_doorkeeper_tables.rb b/db/migrate/20141216155758_create_doorkeeper_tables.rb index b323ffe96f..d1eb25612f 100644 --- a/db/migrate/20141216155758_create_doorkeeper_tables.rb +++ b/db/migrate/20141216155758_create_doorkeeper_tables.rb @@ -1,5 +1,7 @@ # rubocop:disable all class CreateDoorkeeperTables < ActiveRecord::Migration + DOWNTIME = false + def change create_table :oauth_applications do |t| t.string :name, null: false diff --git a/db/migrate/20150108073740_create_application_settings.rb b/db/migrate/20150108073740_create_application_settings.rb index dfa2f76535..101ab31217 100644 --- a/db/migrate/20150108073740_create_application_settings.rb +++ b/db/migrate/20150108073740_create_application_settings.rb @@ -1,5 +1,7 @@ # rubocop:disable all class CreateApplicationSettings < ActiveRecord::Migration + DOWNTIME = false + def change create_table :application_settings do |t| t.integer :default_projects_limit diff --git a/db/migrate/20150313012111_create_subscriptions_table.rb b/db/migrate/20150313012111_create_subscriptions_table.rb index 8adb193b27..a9c0c49c87 100644 --- a/db/migrate/20150313012111_create_subscriptions_table.rb +++ b/db/migrate/20150313012111_create_subscriptions_table.rb @@ -1,15 +1,17 @@ # rubocop:disable all class CreateSubscriptionsTable < ActiveRecord::Migration + DOWNTIME = false + def change create_table :subscriptions do |t| t.integer :user_id t.references :subscribable, polymorphic: true t.boolean :subscribed - + t.timestamps end - add_index :subscriptions, + add_index :subscriptions, [:subscribable_id, :subscribable_type, :user_id], unique: true, name: 'subscriptions_user_id_and_ref_fields' diff --git a/db/migrate/20150806104937_create_abuse_reports.rb b/db/migrate/20150806104937_create_abuse_reports.rb index 3c749b5d9a..2fb4cff35d 100644 --- a/db/migrate/20150806104937_create_abuse_reports.rb +++ b/db/migrate/20150806104937_create_abuse_reports.rb @@ -1,5 +1,7 @@ # rubocop:disable all class CreateAbuseReports < ActiveRecord::Migration + DOWNTIME = false + def change create_table :abuse_reports do |t| t.integer :reporter_id diff --git a/db/migrate/20151103134857_create_lfs_objects.rb b/db/migrate/20151103134857_create_lfs_objects.rb index 745b52e2b2..3c692bcccc 100644 --- a/db/migrate/20151103134857_create_lfs_objects.rb +++ b/db/migrate/20151103134857_create_lfs_objects.rb @@ -1,5 +1,7 @@ # rubocop:disable all class CreateLfsObjects < ActiveRecord::Migration + DOWNTIME = false + def change create_table :lfs_objects do |t| t.string :oid, null: false, unique: true diff --git a/db/migrate/20151103134958_create_lfs_objects_projects.rb b/db/migrate/20151103134958_create_lfs_objects_projects.rb index 3178e85b89..b0f4589353 100644 --- a/db/migrate/20151103134958_create_lfs_objects_projects.rb +++ b/db/migrate/20151103134958_create_lfs_objects_projects.rb @@ -1,5 +1,7 @@ # rubocop:disable all class CreateLfsObjectsProjects < ActiveRecord::Migration + DOWNTIME = false + def change create_table :lfs_objects_projects do |t| t.integer :lfs_object_id, null: false diff --git a/db/migrate/20151105094515_create_releases.rb b/db/migrate/20151105094515_create_releases.rb index 145b8db148..7f98954546 100644 --- a/db/migrate/20151105094515_create_releases.rb +++ b/db/migrate/20151105094515_create_releases.rb @@ -1,5 +1,7 @@ # rubocop:disable all class CreateReleases < ActiveRecord::Migration + DOWNTIME = false + def change create_table :releases do |t| t.string :tag diff --git a/db/migrate/20160212123307_create_tasks.rb b/db/migrate/20160212123307_create_tasks.rb index 20573b0135..84722cc70c 100644 --- a/db/migrate/20160212123307_create_tasks.rb +++ b/db/migrate/20160212123307_create_tasks.rb @@ -1,5 +1,7 @@ # rubocop:disable all class CreateTasks < ActiveRecord::Migration + DOWNTIME = false + def change create_table :tasks do |t| t.references :user, null: false, index: true diff --git a/db/migrate/20160416180807_add_award_emoji.rb b/db/migrate/20160416180807_add_award_emoji.rb index a3bee9b1bc..36906c24c5 100644 --- a/db/migrate/20160416180807_add_award_emoji.rb +++ b/db/migrate/20160416180807_add_award_emoji.rb @@ -1,5 +1,7 @@ # rubocop:disable all class AddAwardEmoji < ActiveRecord::Migration + DOWNTIME = false + def change create_table :award_emoji do |t| t.string :name diff --git a/doc/api/services.md b/doc/api/services.md index 579fdc0c8c..c7f537aceb 100644 --- a/doc/api/services.md +++ b/doc/api/services.md @@ -451,34 +451,7 @@ GET /projects/:id/services/irker ## JIRA -Jira issue tracker - -### Create/Edit JIRA service - -Set JIRA service for a project. - -> Setting `project_url`, `issues_url` and `new_issue_url` will allow a user to easily navigate to the Jira issue tracker. See the [integration doc](http://docs.gitlab.com/ce/integration/external-issue-tracker.html) for details. Support for referencing commits and automatic closing of Jira issues directly from GitLab is [available in GitLab EE.](http://docs.gitlab.com/ee/integration/jira.html) - -``` -PUT /projects/:id/services/jira -``` - -Parameters: - -- `new_issue_url` (**required**) - New Issue url -- `project_url` (**required**) - Project url -- `issues_url` (**required**) - Issue url -- `description` (optional) - Jira issue tracker -- `username` (optional) - Jira username -- `password` (optional) - Jira password - -### Delete JIRA service - -Delete JIRA service for a project. - -``` -DELETE /projects/:id/services/jira -``` +JIRA issue tracker. ### Get JIRA service settings @@ -488,6 +461,39 @@ Get JIRA service settings for a project. GET /projects/:id/services/jira ``` +### Create/Edit JIRA service + +Set JIRA service for a project. + +>**Note:** +Setting `project_url`, `issues_url` and `new_issue_url` will allow a user to +easily navigate to the JIRA issue tracker. See the [integration doc][jira-doc] +for details. + +``` +PUT /projects/:id/services/jira +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `active` | boolean| no | Enable/disable the JIRA service. | +| `project_url` | string | yes | The URL to the JIRA project which is being linked to this GitLab project. It is of the form: `https:///issues/?jql=project=`. | +| `issues_url` | string | yes | The URL to the JIRA project issues overview for the project that is linked to this GitLab project. It is of the form: `https:///browse/:id`. Leave `:id` as-is, it gets replaced by GitLab at runtime.| +| `new_issue_url` | string | yes | This is the URL to create a new issue in JIRA for the project linked to this GitLab project, and it is of the form: `https:///secure/CreateIssue.jspa` | +| `api_url` | string | yes | The base URL of the JIRA API. It may be omitted, in which case GitLab will automatically use API version `2` based on the `project url`. It is of the form: `https:///rest/api/2`. | +| `description` | string | no | A name for the issue tracker. | +| `username` | string | no | The username of the user created to be used with GitLab/JIRA. | +| `password` | string | no | The password of the user created to be used with GitLab/JIRA. | +| `jira_issue_transition_id` | string | no | 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`. | + +### Delete JIRA service + +Remove all previously JIRA settings from a project. + +``` +DELETE /projects/:id/services/jira +``` + ## PivotalTracker Project Management Software (Source Commits Endpoint) @@ -662,3 +668,5 @@ Get JetBrains TeamCity CI service settings for a project. ``` GET /projects/:id/services/teamcity ``` + +[jira-doc]: ../project_services/jira.md diff --git a/doc/api/users.md b/doc/api/users.md index 2b12770d5a..47bb26a19d 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -277,7 +277,9 @@ Parameters: - `id` (required) - The ID of the user -## Current user +## User + +### For normal users Gets currently authenticated user. @@ -321,6 +323,53 @@ GET /user } ``` +### For admins + +Parameters: + +- `sudo` (required) - the ID of a user + +``` +GET /user +``` + +```json +{ + "id": 1, + "username": "john_smith", + "email": "john@example.com", + "name": "John Smith", + "state": "active", + "avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg", + "web_url": "http://localhost:3000/john_smith", + "created_at": "2012-05-23T08:00:58Z", + "is_admin": false, + "bio": null, + "location": null, + "skype": "", + "linkedin": "", + "twitter": "", + "website_url": "", + "organization": "", + "last_sign_in_at": "2012-06-01T11:41:01Z", + "confirmed_at": "2012-05-23T09:05:22Z", + "theme_id": 1, + "color_scheme_id": 2, + "projects_limit": 100, + "current_sign_in_at": "2012-06-02T06:36:55Z", + "identities": [ + {"provider": "github", "extern_uid": "2435223452345"}, + {"provider": "bitbucket", "extern_uid": "john_smith"}, + {"provider": "google_oauth2", "extern_uid": "8776128412476123468721346"} + ], + "can_create_group": true, + "can_create_project": true, + "two_factor_enabled": true, + "external": false, + "private_token": "dd34asd13as" +} +``` + ## List SSH keys Get a list of currently authenticated user's SSH keys. diff --git a/features/steps/shared/authentication.rb b/features/steps/shared/authentication.rb index 735e0ef610..5c3e724746 100644 --- a/features/steps/shared/authentication.rb +++ b/features/steps/shared/authentication.rb @@ -33,6 +33,6 @@ module SharedAuthentication end def current_user - @user || User.first + @user || User.reorder(nil).first end end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 81ce5e807e..42d0206182 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -22,7 +22,7 @@ module API expose :provider, :extern_uid end - class UserFull < User + class UserPublic < User expose :last_sign_in_at expose :confirmed_at expose :email @@ -34,7 +34,7 @@ module API expose :external end - class UserLogin < UserFull + class UserWithPrivateToken < UserPublic expose :private_token end @@ -283,7 +283,7 @@ module API end class SSHKeyWithUser < SSHKey - expose :user, using: Entities::UserFull + expose :user, using: Entities::UserPublic end class Note < Grape::Entity diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 3c9d7b1aae..4edea99d0d 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -7,59 +7,23 @@ module API SUDO_HEADER = "HTTP_SUDO" SUDO_PARAM = :sudo - def private_token - params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER] - end - - def warden - env['warden'] - 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']) - end - - def find_user_by_private_token - token = private_token - return nil unless token.present? - - User.find_by_authentication_token(token) || User.find_by_personal_access_token(token) + def declared_params(options = {}) + options = { include_parent_namespaces: false }.merge(options) + declared(params, options).to_h.symbolize_keys end def current_user - @current_user ||= find_user_by_private_token - @current_user ||= doorkeeper_guard - @current_user ||= find_user_from_warden + return @current_user if defined?(@current_user) - unless @current_user && Gitlab::UserAccess.new(@current_user).allowed? - return nil - end + @current_user = initial_current_user - identifier = sudo_identifier() - - # If the sudo is the current user do nothing - if identifier && !(@current_user.id == identifier || @current_user.username == identifier) - forbidden!('Must be admin to use sudo') unless @current_user.is_admin? - @current_user = User.by_username_or_id(identifier) - not_found!("No user id or username for: #{identifier}") if @current_user.nil? - end + sudo! @current_user end - def sudo_identifier - identifier ||= params[SUDO_PARAM] || env[SUDO_HEADER] - - # Regex for integers - if !!(identifier =~ /\A[0-9]+\z/) - identifier.to_i - else - identifier - end + def sudo? + initial_current_user != current_user end def user_project @@ -70,6 +34,14 @@ module API @available_labels ||= LabelsFinder.new(current_user, project_id: user_project.id).execute end + def find_user(id) + if id =~ /^\d+$/ + User.find_by(id: id) + else + User.find_by(username: id) + end + end + def find_project(id) project = Project.find_with_namespace(id) || Project.find_by(id: id) @@ -122,9 +94,7 @@ module API end def find_project_issue(id) - issue = user_project.issues.find(id) - not_found! unless can?(current_user, :read_issue, issue) - issue + IssuesFinder.new(current_user, project_id: user_project.id).find(id) end def paginate(relation) @@ -192,20 +162,6 @@ module API ActionController::Parameters.new(attrs).permit! end - # Helper method for validating all labels against its names - def validate_label_params(params) - errors = {} - - params[:labels].to_s.split(',').each do |label_name| - label = available_labels.find_or_initialize_by(title: label_name.strip) - next if label.valid? - - errors[label.title] = label.errors - end - - errors - end - # Checks the occurrences of datetime attributes, each attribute if present in the params hash must be in ISO 8601 # format (YYYY-MM-DDTHH:MM:SSZ) or a Bad Request error is invoked. # @@ -394,6 +350,69 @@ module API private + def private_token + params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER] + end + + def warden + env['warden'] + 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']) + end + + def find_user_by_private_token + token = private_token + return nil unless token.present? + + User.find_by_authentication_token(token) || User.find_by_personal_access_token(token) + end + + def initial_current_user + return @initial_current_user if defined?(@initial_current_user) + + @initial_current_user ||= find_user_by_private_token + @initial_current_user ||= doorkeeper_guard + @initial_current_user ||= find_user_from_warden + + unless @initial_current_user && Gitlab::UserAccess.new(@initial_current_user).allowed? + @initial_current_user = nil + end + + @initial_current_user + end + + def sudo! + return unless sudo_identifier + return unless initial_current_user + + unless initial_current_user.is_admin? + forbidden!('Must be admin to use sudo') + end + + # Only private tokens should be used for the SUDO feature + unless private_token == initial_current_user.private_token + forbidden!('Private token must be specified in order to use sudo') + end + + sudoed_user = find_user(sudo_identifier) + + if sudoed_user + @current_user = sudoed_user + else + not_found!("No user id or username for: #{sudo_identifier}") + end + end + + def sudo_identifier + @sudo_identifier ||= params[SUDO_PARAM] || env[SUDO_HEADER] + end + def add_pagination_headers(paginated_data) header 'X-Total', paginated_data.total_count.to_s header 'X-Total-Pages', paginated_data.total_pages.to_s diff --git a/lib/api/issues.rb b/lib/api/issues.rb index c9689e6f8e..26a508f5e1 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -19,6 +19,15 @@ module API def filter_issues_milestone(issues, milestone) issues.includes(:milestone).where('milestones.title' => milestone) end + + def issue_params + new_params = declared(params, include_parent_namespace: false, include_missing: false).to_h + new_params = new_params.with_indifferent_access + new_params.delete(:id) + new_params.delete(:issue_id) + + new_params + end end resource :issues do @@ -86,6 +95,10 @@ module API end end + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects do # Get a list of project issues # @@ -109,7 +122,7 @@ module API # GET /projects/:id/issues?milestone=1.0.0&state=closed # GET /issues?iid=42 get ":id/issues" do - issues = user_project.issues.inc_notes_with_associations.visible_to_user(current_user) + issues = IssuesFinder.new(current_user, project_id: user_project.id).execute.inc_notes_with_associations issues = filter_issues_state(issues, params[:state]) unless params[:state].nil? issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil? issues = filter_by_iid(issues, params[:iid]) unless params[:iid].nil? @@ -152,17 +165,10 @@ module API post ':id/issues' do required_attributes! [:title] - keys = [:title, :description, :assignee_id, :milestone_id, :due_date, :confidential] + keys = [:title, :description, :assignee_id, :milestone_id, :due_date, :confidential, :labels] keys << :created_at if current_user.admin? || user_project.owner == current_user attrs = attributes_for_keys(keys) - # Validate label names in advance - if (errors = validate_label_params(params)).any? - render_api_error!({ labels: errors }, 400) - end - - attrs[:labels] = params[:labels] if params[:labels] - # Convert and filter out invalid confidential flags attrs['confidential'] = to_boolean(attrs['confidential']) attrs.delete('confidential') if attrs['confidential'].nil? @@ -180,41 +186,35 @@ module API end end - # Update an existing issue - # - # Parameters: - # id (required) - The ID of a project - # issue_id (required) - The ID of a project issue - # title (optional) - The title of an issue - # description (optional) - The description of an issue - # assignee_id (optional) - The ID of a user to assign issue - # milestone_id (optional) - The ID of a milestone to assign issue - # labels (optional) - The labels of an issue - # state_event (optional) - The state event of an issue (close|reopen) - # updated_at (optional) - Date time string, ISO 8601 formatted - # due_date (optional) - Date time string in the format YEAR-MONTH-DAY - # confidential (optional) - Boolean parameter if the issue should be confidential - # Example Request: - # PUT /projects/:id/issues/:issue_id + desc 'Update an existing issue' do + success Entities::Issue + end + params do + requires :id, type: String, desc: 'The ID of a project' + requires :issue_id, type: Integer, desc: "The ID of a project issue" + optional :title, type: String, desc: 'The new title of the issue' + optional :description, type: String, desc: 'The description of an issue' + optional :assignee_id, type: Integer, desc: 'The ID of a user to assign issue' + optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign issue' + optional :labels, type: String, desc: 'The labels of an issue' + optional :state_event, type: String, values: ['close', 'reopen'], desc: 'The state event of an issue' + # TODO 9.0, use the Grape DateTime type here + optional :updated_at, type: String, desc: 'Date time string, ISO 8601 formatted' + optional :due_date, type: String, desc: 'Date time string in the format YEAR-MONTH-DAY' + # TODO 9.0, use the Grape boolean type here + optional :confidential, type: String, desc: 'Boolean parameter if the issue should be confidential' + end put ':id/issues/:issue_id' do issue = user_project.issues.find(params[:issue_id]) authorize! :update_issue, issue - keys = [:title, :description, :assignee_id, :milestone_id, :state_event, :due_date, :confidential] - keys << :updated_at if current_user.admin? || user_project.owner == current_user - attrs = attributes_for_keys(keys) - - # Validate label names in advance - if (errors = validate_label_params(params)).any? - render_api_error!({ labels: errors }, 400) - end - - attrs[:labels] = params[:labels] if params[:labels] # Convert and filter out invalid confidential flags - attrs['confidential'] = to_boolean(attrs['confidential']) - attrs.delete('confidential') if attrs['confidential'].nil? + params[:confidential] = to_boolean(params[:confidential]) + params.delete(:confidential) if params[:confidential].nil? - issue = ::Issues::UpdateService.new(user_project, current_user, attrs).execute(issue) + params.delete(:updated_at) unless current_user.admin? || user_project.owner == current_user + + issue = ::Issues::UpdateService.new(user_project, current_user, issue_params).execute(issue) if issue.valid? present issue, with: Entities::Issue, current_user: current_user diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index bf8504e110..20bb9e73dc 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -79,12 +79,7 @@ module API post ":id/merge_requests" do authorize! :create_merge_request, user_project required_attributes! [:source_branch, :target_branch, :title] - attrs = attributes_for_keys [:source_branch, :target_branch, :assignee_id, :title, :target_project_id, :description, :milestone_id] - - # Validate label names in advance - if (errors = validate_label_params(params)).any? - render_api_error!({ labels: errors }, 400) - end + attrs = attributes_for_keys [:source_branch, :target_branch, :assignee_id, :title, :target_project_id, :description, :milestone_id, :labels] attrs[:labels] = params[:labels] if params[:labels] @@ -112,6 +107,9 @@ module API # Routing "merge_request/:merge_request_id/..." is DEPRECATED and WILL BE REMOVED in version 9.0 # Use "merge_requests/:merge_request_id/..." instead. # + params do + requires :id, type: String, desc: 'The ID of a project' + end [":id/merge_request/:merge_request_id", ":id/merge_requests/:merge_request_id"].each do |path| # Show MR # @@ -162,23 +160,20 @@ module API present merge_request, with: Entities::MergeRequestChanges, current_user: current_user end - # Update MR - # - # Parameters: - # id (required) - The ID of a project - # merge_request_id (required) - ID of MR - # target_branch - The target branch - # assignee_id - Assignee user ID - # title - Title of MR - # state_event - Status of MR. (close|reopen|merge) - # description - Description of MR - # labels (optional) - Labels for a MR as a comma-separated list - # milestone_id (optional) - Milestone ID - # Example: - # PUT /projects/:id/merge_requests/:merge_request_id - # + desc 'Update a merge request' do + success Entities::MergeRequest + end + params do + requires :merge_request_id, type: Integer, desc: 'The ID of a merge request' + optional :target_branch, type: String, desc: 'The new target branch' + optional :assignee_id, type: Integer, desc: 'The assignees user ID' + optional :title, type: String, desc: 'The new title for the merge request' + optional :state_event, type: String, values: ['close', 'reopen', 'merge'], desc: 'The state of the merge request' + optional :description, type: String, desc: 'The description, with markdown support' + optional :labels, type: String, desc: 'Labels for a MR as a comma-separated list' + optional :milestone_id, type: Integer, desc: 'The ID of the new milestone' + end put path do - attrs = attributes_for_keys [:target_branch, :assignee_id, :title, :state_event, :description, :milestone_id] merge_request = user_project.merge_requests.find(params[:merge_request_id]) authorize! :update_merge_request, merge_request @@ -187,14 +182,10 @@ module API render_api_error!('Source branch cannot be changed', 400) end - # Validate label names in advance - if (errors = validate_label_params(params)).any? - render_api_error!({ labels: errors }, 400) - end + mr_params = declared(params, include_missing: false, include_parent_namespace: false).with_indifferent_access + mr_params.delete(:merge_request_id) - attrs[:labels] = params[:labels] if params[:labels] - - merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, attrs).execute(merge_request) + merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, mr_params).execute(merge_request) if merge_request.valid? present merge_request, with: Entities::MergeRequest, current_user: current_user diff --git a/lib/api/session.rb b/lib/api/session.rb index 55ec66a6d6..af863f9efc 100644 --- a/lib/api/session.rb +++ b/lib/api/session.rb @@ -15,7 +15,7 @@ module API return unauthorized! unless user return render_api_error!('401 Unauthorized. You have 2FA enabled. Please use a personal access token to access the API', 401) if user.two_factor_enabled? - present user, with: Entities::UserLogin + present user, with: Entities::UserWithPrivateToken end end end diff --git a/lib/api/users.rb b/lib/api/users.rb index c28e07a76b..029d4a1870 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -25,7 +25,7 @@ module API end if current_user.is_admin? - present @users, with: Entities::UserFull + present @users, with: Entities::UserPublic else present @users, with: Entities::UserBasic end @@ -41,7 +41,7 @@ module API @user = User.find(params[:id]) if current_user && current_user.is_admin? - present @user, with: Entities::UserFull + present @user, with: Entities::UserPublic elsif can?(current_user, :read_user, @user) present @user, with: Entities::User else @@ -88,7 +88,7 @@ module API end if user.save - present user, with: Entities::UserFull + present user, with: Entities::UserPublic else conflict!('Email has already been taken') if User. where(email: user.email). @@ -151,7 +151,7 @@ module API end if user.update_attributes(attrs) - present user, with: Entities::UserFull + present user, with: Entities::UserPublic else render_validation_error!(user) end @@ -349,7 +349,7 @@ module API # Example Request: # GET /user get do - present @current_user, with: Entities::UserFull + present current_user, with: sudo? ? Entities::UserWithPrivateToken : Entities::UserPublic end # Get currently authenticated user's keys diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb index 5b9cfaeb2f..9803fa4cee 100644 --- a/lib/gitlab/project_search_results.rb +++ b/lib/gitlab/project_search_results.rb @@ -69,7 +69,7 @@ module Gitlab end def notes - project.notes.user.search(query, as_user: @current_user).order('updated_at DESC') + @notes ||= NotesFinder.new(project, @current_user, search: query).execute.user.order('updated_at DESC') end def commits diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb index 2690938fe8..3521299269 100644 --- a/lib/gitlab/search_results.rb +++ b/lib/gitlab/search_results.rb @@ -50,7 +50,7 @@ module Gitlab end def issues - issues = Issue.visible_to_user(current_user).where(project_id: project_ids_relation) + issues = IssuesFinder.new(current_user).execute.where(project_id: project_ids_relation) if query =~ /#(\d+)\z/ issues = issues.where(iid: $1) @@ -68,7 +68,7 @@ module Gitlab end def merge_requests - merge_requests = MergeRequest.in_projects(project_ids_relation) + merge_requests = MergeRequestsFinder.new(current_user).execute.in_projects(project_ids_relation) if query =~ /[#!](\d+)\z/ merge_requests = merge_requests.where(iid: $1) else diff --git a/lib/gitlab/sql/union.rb b/lib/gitlab/sql/union.rb index 1cd89b3a9c..222021e880 100644 --- a/lib/gitlab/sql/union.rb +++ b/lib/gitlab/sql/union.rb @@ -22,9 +22,7 @@ module Gitlab # By using "unprepared_statements" we remove the usage of placeholders # (thus fixing this problem), at a slight performance cost. fragments = ActiveRecord::Base.connection.unprepared_statement do - @relations.map do |rel| - rel.reorder(nil).to_sql - end + @relations.map { |rel| rel.reorder(nil).to_sql }.reject(&:blank?) end fragments.join("\nUNION\n") diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb index 52d13fb6f9..1c2b0a4a45 100644 --- a/spec/controllers/projects/blob_controller_spec.rb +++ b/spec/controllers/projects/blob_controller_spec.rb @@ -36,4 +36,53 @@ describe Projects::BlobController do end end end + + describe 'PUT update' do + let(:default_params) do + { + namespace_id: project.namespace.to_param, + project_id: project.to_param, + id: 'master/CHANGELOG', + target_branch: 'master', + content: 'Added changes', + commit_message: 'Update CHANGELOG' + } + end + + def blob_after_edit_path + namespace_project_blob_path(project.namespace, project, 'master/CHANGELOG') + end + + it 'redirects to blob' do + put :update, default_params + + expect(response).to redirect_to(blob_after_edit_path) + end + + context '?from_merge_request_iid' do + let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } + let(:mr_params) { default_params.merge(from_merge_request_iid: merge_request.iid) } + + it 'redirects to MR diff' do + put :update, mr_params + + after_edit_path = diffs_namespace_project_merge_request_path(project.namespace, project, merge_request) + file_anchor = "#file-path-#{Digest::SHA1.hexdigest('CHANGELOG')}" + expect(response).to redirect_to(after_edit_path + file_anchor) + end + + context "when user doesn't have access" do + before do + other_project = create(:empty_project) + merge_request.update!(source_project: other_project, target_project: other_project) + end + + it "it redirect to blob" do + put :update, mr_params + + expect(response).to redirect_to(blob_after_edit_path) + end + end + end + end end diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb index 644de308c6..fb701337e5 100644 --- a/spec/controllers/projects/branches_controller_spec.rb +++ b/spec/controllers/projects/branches_controller_spec.rb @@ -88,6 +88,24 @@ describe Projects::BranchesController do branch_name: branch, issue_iid: issue.iid end + + context 'without issue feature access' do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + project.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE) + project.team.truncate + end + + it "doesn't post a system note" do + expect(SystemNoteService).not_to receive(:new_issue_branch) + + post :create, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + branch_name: branch, + issue_iid: issue.iid + end + end end end diff --git a/spec/controllers/projects/todo_controller_spec.rb b/spec/controllers/projects/todo_controller_spec.rb index 936320a370..415c264e0d 100644 --- a/spec/controllers/projects/todo_controller_spec.rb +++ b/spec/controllers/projects/todo_controller_spec.rb @@ -4,7 +4,7 @@ describe Projects::TodosController do include ApiHelpers let(:user) { create(:user) } - let(:project) { create(:project) } + let(:project) { create(:empty_project) } let(:issue) { create(:issue, project: project) } let(:merge_request) { create(:merge_request, source_project: project) } @@ -42,7 +42,7 @@ describe Projects::TodosController do end end - context 'when not authorized' do + context 'when not authorized for project' do it 'does not create todo for issue that user has no access to' do sign_in(user) expect do @@ -60,6 +60,19 @@ describe Projects::TodosController do expect(response).to have_http_status(302) end end + + context 'when not authorized for issue' do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + project.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE) + sign_in(user) + end + + it "doesn't create todo" do + expect{ go }.not_to change { user.todos.count } + expect(response).to have_http_status(404) + end + end end end @@ -97,7 +110,7 @@ describe Projects::TodosController do end end - context 'when not authorized' do + context 'when not authorized for project' do it 'does not create todo for merge request user has no access to' do sign_in(user) expect do @@ -115,6 +128,19 @@ describe Projects::TodosController do expect(response).to have_http_status(302) end end + + context 'when not authorized for merge_request' do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + project.project_feature.update!(merge_requests_access_level: ProjectFeature::PRIVATE) + sign_in(user) + end + + it "doesn't create todo" do + expect{ go }.not_to change { user.todos.count } + expect(response).to have_http_status(404) + end + end end end end diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb new file mode 100644 index 0000000000..b7bb929071 --- /dev/null +++ b/spec/controllers/search_controller_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' + +describe SearchController do + let(:user) { create(:user) } + let(:project) { create(:empty_project, :public) } + + before do + sign_in(user) + end + + it 'finds issue comments' do + project = create(:empty_project, :public) + note = create(:note_on_issue, project: project) + + get :show, project_id: project.id, scope: 'notes', search: note.note + + expect(assigns[:search_objects].first).to eq note + end + + context 'on restricted projects' do + context 'when signed out' do + before { sign_out(user) } + + it "doesn't expose comments on issues" do + project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE) + note = create(:note_on_issue, project: project) + + get :show, project_id: project.id, scope: 'notes', search: note.note + + expect(assigns[:search_objects].count).to eq(0) + end + end + + it "doesn't expose comments on issues" do + project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE) + note = create(:note_on_issue, project: project) + + get :show, project_id: project.id, scope: 'notes', search: note.note + + expect(assigns[:search_objects].count).to eq(0) + end + + it "doesn't expose comments on merge_requests" do + project = create(:empty_project, :public, merge_requests_access_level: ProjectFeature::PRIVATE) + note = create(:note_on_merge_request, project: project) + + get :show, project_id: project.id, scope: 'notes', search: note.note + + expect(assigns[:search_objects].count).to eq(0) + end + + it "doesn't expose comments on snippets" do + project = create(:empty_project, :public, snippets_access_level: ProjectFeature::PRIVATE) + note = create(:note_on_project_snippet, project: project) + + get :show, project_id: project.id, scope: 'notes', search: note.note + + expect(assigns[:search_objects].count).to eq(0) + end + end +end diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb index 6919002ded..a10ba62976 100644 --- a/spec/factories/notes.rb +++ b/spec/factories/notes.rb @@ -67,7 +67,7 @@ FactoryGirl.define do end trait :on_project_snippet do - noteable { create(:snippet, project: project) } + noteable { create(:project_snippet, project: project) } end trait :system do diff --git a/spec/features/projects/blobs/edit_spec.rb b/spec/features/projects/blobs/edit_spec.rb new file mode 100644 index 0000000000..a820d07ab3 --- /dev/null +++ b/spec/features/projects/blobs/edit_spec.rb @@ -0,0 +1,45 @@ +require 'spec_helper' + +feature 'Editing file blob', feature: true, js: true do + include WaitForAjax + + given(:user) { create(:user) } + given(:role) { :developer } + given(:merge_request) { create(:merge_request, source_branch: 'feature', target_branch: 'master') } + given(:project) { merge_request.target_project } + + background do + login_as(user) + project.team << [user, role] + end + + def edit_and_commit + wait_for_ajax + first('.file-actions').click_link 'Edit' + execute_script('ace.edit("editor").setValue("class NextFeature\nend\n")') + click_button 'Commit Changes' + end + + context 'from MR diff' do + before do + visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request) + edit_and_commit + end + + scenario 'returns me to the mr' do + expect(page).to have_content(merge_request.title) + end + end + + context 'from blob file path' do + before do + visit namespace_project_blob_path(project.namespace, project, '/feature/files/ruby/feature.rb') + edit_and_commit + end + + scenario 'updates content' do + expect(page).to have_content 'successfully committed' + expect(page).to have_content 'NextFeature' + end + end +end diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index 40bccb8e50..97737d7ddc 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -10,22 +10,23 @@ describe IssuesFinder do let(:issue1) { create(:issue, author: user, assignee: user, project: project1, milestone: milestone, title: 'gitlab') } let(:issue2) { create(:issue, author: user, assignee: user, project: project2, description: 'gitlab') } let(:issue3) { create(:issue, author: user2, assignee: user2, project: project2) } - let!(:label_link) { create(:label_link, label: label, target: issue2) } - - before do - project1.team << [user, :master] - project2.team << [user, :developer] - project2.team << [user2, :developer] - - issue1 - issue2 - issue3 - end describe '#execute' do + let(:closed_issue) { create(:issue, author: user2, assignee: user2, project: project2, state: 'closed') } + let!(:label_link) { create(:label_link, label: label, target: issue2) } let(:search_user) { user } let(:params) { {} } - let(:issues) { IssuesFinder.new(search_user, params.merge(scope: scope, state: 'opened')).execute } + let(:issues) { IssuesFinder.new(search_user, params.reverse_merge(scope: scope, state: 'opened')).execute } + + before do + project1.team << [user, :master] + project2.team << [user, :developer] + project2.team << [user2, :developer] + + issue1 + issue2 + issue3 + end context 'scope: all' do let(:scope) { 'all' } @@ -143,6 +144,40 @@ describe IssuesFinder do end end + context 'filtering by state' do + context 'with opened' do + let(:params) { { state: 'opened' } } + + it 'returns only opened issues' do + expect(issues).to contain_exactly(issue1, issue2, issue3) + end + end + + context 'with closed' do + let(:params) { { state: 'closed' } } + + it 'returns only closed issues' do + expect(issues).to contain_exactly(closed_issue) + end + end + + context 'with all' do + let(:params) { { state: 'all' } } + + it 'returns all issues' do + expect(issues).to contain_exactly(issue1, issue2, issue3, closed_issue) + end + end + + context 'with invalid state' do + let(:params) { { state: 'invalid_state' } } + + it 'returns all issues' do + expect(issues).to contain_exactly(issue1, issue2, issue3, closed_issue) + end + end + end + context 'when the user is unauthorized' do let(:search_user) { nil } @@ -158,6 +193,15 @@ describe IssuesFinder do expect(issues).to contain_exactly(issue2, issue3) end end + + it 'finds issues user can access due to group' do + group = create(:group) + project = create(:empty_project, group: group) + issue = create(:issue, project: project) + group.add_user(user, :owner) + + expect(issues).to include(issue) + end end context 'personal scope' do @@ -175,5 +219,43 @@ describe IssuesFinder do end end end + + context 'when project restricts issues' do + let(:scope) { nil } + + it "doesn't return team-only issues to non team members" do + project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE) + issue = create(:issue, project: project) + + expect(issues).not_to include(issue) + end + + it "doesn't return issues if feature disabled" do + [project1, project2].each do |project| + project.project_feature.update!(issues_access_level: ProjectFeature::DISABLED) + end + + expect(issues.count).to eq 0 + end + end + end + + describe '.not_restricted_by_confidentiality' do + let(:authorized_user) { create(:user) } + let(:project) { create(:empty_project, namespace: authorized_user.namespace) } + let!(:public_issue) { create(:issue, project: project) } + let!(:confidential_issue) { create(:issue, project: project, confidential: true) } + + it 'returns non confidential issues for nil user' do + expect(IssuesFinder.send(:not_restricted_by_confidentiality, nil)).to include(public_issue) + end + + it 'returns non confidential issues for user not authorized for the issues projects' do + expect(IssuesFinder.send(:not_restricted_by_confidentiality, user)).to include(public_issue) + end + + it 'returns all issues for user authorized for the issues projects' do + expect(IssuesFinder.send(:not_restricted_by_confidentiality, authorized_user)).to include(public_issue, confidential_issue) + end end end diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb index 7c6860372c..4d21254323 100644 --- a/spec/finders/notes_finder_spec.rb +++ b/spec/finders/notes_finder_spec.rb @@ -2,59 +2,203 @@ require 'spec_helper' describe NotesFinder do let(:user) { create :user } - let(:project) { create :project } - let(:note1) { create :note_on_commit, project: project } - let(:note2) { create :note_on_commit, project: project } - let(:commit) { note1.noteable } + let(:project) { create(:empty_project) } before do project.team << [user, :master] end describe '#execute' do - let(:params) { { target_id: commit.id, target_type: 'commit', last_fetched_at: 1.hour.ago.to_i } } + it 'finds notes on snippets when project is public and user isnt a member' - before do - note1 - note2 + it 'finds notes on merge requests' do + create(:note_on_merge_request, project: project) + + notes = described_class.new(project, user).execute + + expect(notes.count).to eq(1) end - it 'finds all notes' do - notes = NotesFinder.new.execute(project, user, params) - expect(notes.size).to eq(2) + it 'finds notes on snippets' do + create(:note_on_project_snippet, project: project) + + notes = described_class.new(project, user).execute + + expect(notes.count).to eq(1) end - it 'raises an exception for an invalid target_type' do - params.merge!(target_type: 'invalid') - expect { NotesFinder.new.execute(project, user, params) }.to raise_error('invalid target_type') + it "excludes notes on commits the author can't download" do + project = create(:project, :private) + note = create(:note_on_commit, project: project) + params = { target_type: 'commit', target_id: note.noteable.id } + + notes = described_class.new(project, create(:user), params).execute + + expect(notes.count).to eq(0) end - it 'filters out old notes' do - note2.update_attribute(:updated_at, 2.hours.ago) - notes = NotesFinder.new.execute(project, user, params) - expect(notes).to eq([note1]) + it 'succeeds when no notes found' do + notes = described_class.new(project, create(:user)).execute + + expect(notes.count).to eq(0) end - context 'confidential issue notes' do + context 'on restricted projects' do + let(:project) do + create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE, + snippets_access_level: ProjectFeature::PRIVATE, + merge_requests_access_level: ProjectFeature::PRIVATE) + end + + it 'publicly excludes notes on merge requests' do + create(:note_on_merge_request, project: project) + + notes = described_class.new(project, create(:user)).execute + + expect(notes.count).to eq(0) + end + + it 'publicly excludes notes on issues' do + create(:note_on_issue, project: project) + + notes = described_class.new(project, create(:user)).execute + + expect(notes.count).to eq(0) + end + + it 'publicly excludes notes on snippets' do + create(:note_on_project_snippet, project: project) + + notes = described_class.new(project, create(:user)).execute + + expect(notes.count).to eq(0) + end + end + + context 'for target' do + let(:project) { create(:project) } + let(:note1) { create :note_on_commit, project: project } + let(:note2) { create :note_on_commit, project: project } + let(:commit) { note1.noteable } + let(:params) { { target_id: commit.id, target_type: 'commit', last_fetched_at: 1.hour.ago.to_i } } + + before do + note1 + note2 + end + + it 'finds all notes' do + notes = described_class.new(project, user, params).execute + expect(notes.size).to eq(2) + end + + it 'finds notes on merge requests' do + note = create(:note_on_merge_request, project: project) + params = { target_type: 'merge_request', target_id: note.noteable.id } + + notes = described_class.new(project, user, params).execute + + expect(notes).to include(note) + end + + it 'finds notes on snippets' do + note = create(:note_on_project_snippet, project: project) + params = { target_type: 'snippet', target_id: note.noteable.id } + + notes = described_class.new(project, user, params).execute + + expect(notes.count).to eq(1) + end + + it 'raises an exception for an invalid target_type' do + params.merge!(target_type: 'invalid') + expect { described_class.new(project, user, params).execute }.to raise_error('invalid target_type') + end + + it 'filters out old notes' do + note2.update_attribute(:updated_at, 2.hours.ago) + notes = described_class.new(project, user, params).execute + expect(notes).to eq([note1]) + end + + context 'confidential issue notes' do + let(:confidential_issue) { create(:issue, :confidential, project: project, author: user) } + let!(:confidential_note) { create(:note, noteable: confidential_issue, project: confidential_issue.project) } + + let(:params) { { target_id: confidential_issue.id, target_type: 'issue', last_fetched_at: 1.hour.ago.to_i } } + + it 'returns notes if user can see the issue' do + expect(described_class.new(project, user, params).execute).to eq([confidential_note]) + end + + it 'raises an error if user can not see the issue' do + user = create(:user) + expect { described_class.new(project, user, params).execute }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'raises an error for project members with guest role' do + user = create(:user) + project.team << [user, :guest] + + expect { described_class.new(project, user, params).execute }.to raise_error(ActiveRecord::RecordNotFound) + end + end + end + end + + describe '.search' do + let(:project) { create(:empty_project, :public) } + let(:note) { create(:note_on_issue, note: 'WoW', project: project) } + + it 'returns notes with matching content' do + expect(described_class.new(note.project, nil, search: note.note).execute).to eq([note]) + end + + it 'returns notes with matching content regardless of the casing' do + expect(described_class.new(note.project, nil, search: 'WOW').execute).to eq([note]) + end + + it 'returns commit notes user can access' do + note = create(:note_on_commit, project: project) + + expect(described_class.new(note.project, create(:user), search: note.note).execute).to eq([note]) + end + + context "confidential issues" do + let(:user) { create(:user) } let(:confidential_issue) { create(:issue, :confidential, project: project, author: user) } - let!(:confidential_note) { create(:note, noteable: confidential_issue, project: confidential_issue.project) } + let(:confidential_note) { create(:note, note: "Random", noteable: confidential_issue, project: confidential_issue.project) } - let(:params) { { target_id: confidential_issue.id, target_type: 'issue', last_fetched_at: 1.hour.ago.to_i } } - - it 'returns notes if user can see the issue' do - expect(NotesFinder.new.execute(project, user, params)).to eq([confidential_note]) + it "returns notes with matching content if user can see the issue" do + expect(described_class.new(confidential_note.project, user, search: confidential_note.note).execute).to eq([confidential_note]) end - it 'raises an error if user can not see the issue' do + it "does not return notes with matching content if user can not see the issue" do user = create(:user) - expect { NotesFinder.new.execute(project, user, params) }.to raise_error(ActiveRecord::RecordNotFound) + expect(described_class.new(confidential_note.project, user, search: confidential_note.note).execute).to be_empty end - it 'raises an error for project members with guest role' do + it "does not return notes with matching content for project members with guest role" do user = create(:user) project.team << [user, :guest] + expect(described_class.new(confidential_note.project, user, search: confidential_note.note).execute).to be_empty + end - expect { NotesFinder.new.execute(project, user, params) }.to raise_error(ActiveRecord::RecordNotFound) + it "does not return notes with matching content for unauthenticated users" do + expect(described_class.new(confidential_note.project, nil, search: confidential_note.note).execute).to be_empty + end + end + + context 'inlines SQL filters on subqueries for performance' do + let(:sql) { described_class.new(note.project, nil, search: note.note).execute.to_sql } + let(:number_of_noteable_types) { 4 } + + specify 'project_id check' do + expect(sql.scan(/project_id/).count).to be >= (number_of_noteable_types + 2) + end + + specify 'search filter' do + expect(sql.scan(/LIKE/).count).to be >= number_of_noteable_types end end end diff --git a/spec/fixtures/api/schemas/user/login.json b/spec/fixtures/api/schemas/user/login.json new file mode 100644 index 0000000000..e6c1d9c9d8 --- /dev/null +++ b/spec/fixtures/api/schemas/user/login.json @@ -0,0 +1,37 @@ +{ + "type": "object", + "required": [ + "id", + "username", + "email", + "name", + "state", + "avatar_url", + "web_url", + "created_at", + "is_admin", + "bio", + "location", + "skype", + "linkedin", + "twitter", + "website_url", + "organization", + "last_sign_in_at", + "confirmed_at", + "theme_id", + "color_scheme_id", + "projects_limit", + "current_sign_in_at", + "identities", + "can_create_group", + "can_create_project", + "two_factor_enabled", + "external", + "private_token" + ], + "properties": { + "$ref": "full.json", + "private_token": { "type": "string" } + } +} diff --git a/spec/fixtures/api/schemas/user/public.json b/spec/fixtures/api/schemas/user/public.json new file mode 100644 index 0000000000..dbd5d32e89 --- /dev/null +++ b/spec/fixtures/api/schemas/user/public.json @@ -0,0 +1,79 @@ +{ + "type": "object", + "required": [ + "id", + "username", + "email", + "name", + "state", + "avatar_url", + "web_url", + "created_at", + "is_admin", + "bio", + "location", + "skype", + "linkedin", + "twitter", + "website_url", + "organization", + "last_sign_in_at", + "confirmed_at", + "theme_id", + "color_scheme_id", + "projects_limit", + "current_sign_in_at", + "identities", + "can_create_group", + "can_create_project", + "two_factor_enabled", + "external" + ], + "properties": { + "id": { "type": "integer" }, + "username": { "type": "string" }, + "email": { + "type": "string", + "pattern": "^[^@]+@[^@]+$" + }, + "name": { "type": "string" }, + "state": { + "type": "string", + "enum": ["active", "blocked"] + }, + "avatar_url": { "type": "string" }, + "web_url": { "type": "string" }, + "created_at": { "type": "date" }, + "is_admin": { "type": "boolean" }, + "bio": { "type": ["string", "null"] }, + "location": { "type": ["string", "null"] }, + "skype": { "type": "string" }, + "linkedin": { "type": "string" }, + "twitter": { "type": "string "}, + "website_url": { "type": "string" }, + "organization": { "type": ["string", "null"] }, + "last_sign_in_at": { "type": "date" }, + "confirmed_at": { "type": ["date", "null"] }, + "theme_id": { "type": "integer" }, + "color_scheme_id": { "type": "integer" }, + "projects_limit": { "type": "integer" }, + "current_sign_in_at": { "type": "date" }, + "identities": { + "type": "array", + "items": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "enum": ["github", "bitbucket", "google_oauth2"] + }, + "extern_uid": { "type": ["number", "string"] } + } + } + }, + "can_create_group": { "type": "boolean" }, + "can_create_project": { "type": "boolean" }, + "two_factor_enabled": { "type": "boolean" }, + "external": { "type": "boolean" } + } +} diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb index 2f9291afc3..77841e8522 100644 --- a/spec/helpers/preferences_helper_spec.rb +++ b/spec/helpers/preferences_helper_spec.rb @@ -85,4 +85,45 @@ describe PreferencesHelper do and_return(double('user', messages)) end end + + describe '#default_project_view' do + context 'user not signed in' do + before do + helper.instance_variable_set(:@project, project) + stub_user + end + + context 'when repository is empty' do + let(:project) { create(:project_empty_repo, :public) } + + it 'returns activity if user has repository access' do + allow(helper).to receive(:can?).with(nil, :download_code, project).and_return(true) + + expect(helper.default_project_view).to eq('activity') + end + + it 'returns activity if user does not have repository access' do + allow(helper).to receive(:can?).with(nil, :download_code, project).and_return(false) + + expect(helper.default_project_view).to eq('activity') + end + end + + context 'when repository is not empty' do + let(:project) { create(:project, :public) } + + it 'returns readme if user has repository access' do + allow(helper).to receive(:can?).with(nil, :download_code, project).and_return(true) + + expect(helper.default_project_view).to eq('readme') + end + + it 'returns activity if user does not have repository access' do + allow(helper).to receive(:can?).with(nil, :download_code, project).and_return(false) + + expect(helper.default_project_view).to eq('activity') + end + end + end + end end diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb index 29abb4d4d0..ade489cbf4 100644 --- a/spec/lib/gitlab/project_search_results_spec.rb +++ b/spec/lib/gitlab/project_search_results_spec.rb @@ -22,6 +22,14 @@ describe Gitlab::ProjectSearchResults, lib: true do it { expect(results.query).to eq('hello world') } end + it 'does not list issues on private projects' do + issue = create(:issue, project: project) + + results = described_class.new(user, project, issue.title) + + expect(results.objects('issues')).not_to include issue + end + describe 'confidential issues' do let(:query) { 'issue' } let(:author) { create(:user) } @@ -29,6 +37,7 @@ describe Gitlab::ProjectSearchResults, lib: true do let(:non_member) { create(:user) } let(:member) { create(:user) } let(:admin) { create(:admin) } + let(:project) { create(:empty_project, :internal) } let!(:issue) { create(:issue, project: project, title: 'Issue 1') } let!(:security_issue_1) { create(:issue, :confidential, project: project, title: 'Security issue 1', author: author) } let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project, assignee: assignee) } @@ -97,4 +106,33 @@ describe Gitlab::ProjectSearchResults, lib: true do expect(results.issues_count).to eq 3 end end + + describe 'notes search' do + it 'lists notes' do + project = create(:empty_project, :public) + note = create(:note, project: project) + + results = described_class.new(user, project, note.note) + + expect(results.objects('notes')).to include note + end + + it "doesn't list issue notes when access is restricted" do + project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE) + note = create(:note_on_issue, project: project) + + results = described_class.new(user, project, note.note) + + expect(results.objects('notes')).not_to include note + end + + it "doesn't list merge_request notes when access is restricted" do + project = create(:empty_project, :public, merge_requests_access_level: ProjectFeature::PRIVATE) + note = create(:note_on_merge_request, project: project) + + results = described_class.new(user, project, note.note) + + expect(results.objects('notes')).not_to include note + end + end end diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb index dfbefad636..9614aad3e7 100644 --- a/spec/lib/gitlab/search_results_spec.rb +++ b/spec/lib/gitlab/search_results_spec.rb @@ -12,35 +12,57 @@ describe Gitlab::SearchResults do let!(:milestone) { create(:milestone, project: project, title: 'foo') } let(:results) { described_class.new(user, Project.all, 'foo') } - describe '#projects_count' do - it 'returns the total amount of projects' do - expect(results.projects_count).to eq(1) + context 'as a user with access' do + before do + project.team << [user, :developer] + end + + describe '#projects_count' do + it 'returns the total amount of projects' do + expect(results.projects_count).to eq(1) + end + end + + describe '#issues_count' do + it 'returns the total amount of issues' do + expect(results.issues_count).to eq(1) + end + end + + describe '#merge_requests_count' do + it 'returns the total amount of merge requests' do + expect(results.merge_requests_count).to eq(1) + end + end + + describe '#milestones_count' do + it 'returns the total amount of milestones' do + expect(results.milestones_count).to eq(1) + end + end + + it 'includes merge requests from source and target projects' do + forked_project = create(:empty_project, forked_from_project: project) + merge_request_2 = create(:merge_request, target_project: project, source_project: forked_project, title: 'foo') + + results = described_class.new(user, Project.where(id: forked_project.id), 'foo') + + expect(results.objects('merge_requests')).to include merge_request_2 end end - describe '#issues_count' do - it 'returns the total amount of issues' do - expect(results.issues_count).to eq(1) - end - end + it 'does not list issues on private projects' do + private_project = create(:empty_project, :private) + issue = create(:issue, project: private_project, title: 'foo') - describe '#merge_requests_count' do - it 'returns the total amount of merge requests' do - expect(results.merge_requests_count).to eq(1) - end - end - - describe '#milestones_count' do - it 'returns the total amount of milestones' do - expect(results.milestones_count).to eq(1) - end + expect(results.objects('issues')).not_to include issue end describe 'confidential issues' do - let(:project_1) { create(:empty_project) } - let(:project_2) { create(:empty_project) } - let(:project_3) { create(:empty_project) } - let(:project_4) { create(:empty_project) } + let(:project_1) { create(:empty_project, :internal) } + let(:project_2) { create(:empty_project, :internal) } + let(:project_3) { create(:empty_project, :internal) } + let(:project_4) { create(:empty_project, :internal) } let(:query) { 'issue' } let(:limit_projects) { Project.where(id: [project_1.id, project_2.id, project_3.id]) } let(:author) { create(:user) } @@ -139,4 +161,11 @@ describe Gitlab::SearchResults do expect(results.issues_count).to eq 5 end end + + it 'does not list merge requests on projects with limited access' do + project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + project.project_feature.update!(merge_requests_access_level: ProjectFeature::PRIVATE) + + expect(results.objects('merge_requests')).not_to include merge_request + end end diff --git a/spec/lib/gitlab/sql/union_spec.rb b/spec/lib/gitlab/sql/union_spec.rb index 0cdbab8754..849edb0947 100644 --- a/spec/lib/gitlab/sql/union_spec.rb +++ b/spec/lib/gitlab/sql/union_spec.rb @@ -1,16 +1,26 @@ require 'spec_helper' describe Gitlab::SQL::Union, lib: true do + let(:relation_1) { User.where(email: 'alice@example.com').select(:id) } + let(:relation_2) { User.where(email: 'bob@example.com').select(:id) } + + def to_sql(relation) + relation.reorder(nil).to_sql + end + describe '#to_sql' do it 'returns a String joining relations together using a UNION' do - rel1 = User.where(email: 'alice@example.com') - rel2 = User.where(email: 'bob@example.com') - union = described_class.new([rel1, rel2]) + union = described_class.new([relation_1, relation_2]) - sql1 = rel1.reorder(nil).to_sql - sql2 = rel2.reorder(nil).to_sql + expect(union.to_sql).to eq("#{to_sql(relation_1)}\nUNION\n#{to_sql(relation_2)}") + end - expect(union.to_sql).to eq("#{sql1}\nUNION\n#{sql2}") + it 'skips Model.none segements' do + empty_relation = User.none + union = described_class.new([empty_relation, relation_1, relation_2]) + + expect{User.where("users.id IN (#{union.to_sql})").to_a}.not_to raise_error + expect(union.to_sql).to eq("#{to_sql(relation_1)}\nUNION\n#{to_sql(relation_2)}") end end end diff --git a/spec/models/commit_range_spec.rb b/spec/models/commit_range_spec.rb index c41359b55a..6167102a05 100644 --- a/spec/models/commit_range_spec.rb +++ b/spec/models/commit_range_spec.rb @@ -137,26 +137,25 @@ describe CommitRange, models: true do end describe '#has_been_reverted?' do - it 'returns true if the commit has been reverted' do - issue = create(:issue) + let(:issue) { create(:issue) } + let(:user) { issue.author } + it 'returns true if the commit has been reverted' do create(:note_on_issue, noteable: issue, system: true, - note: commit1.revert_description, + note: commit1.revert_description(user), project: issue.project) expect_any_instance_of(Commit).to receive(:reverts_commit?). - with(commit1). + with(commit1, user). and_return(true) - expect(commit1.has_been_reverted?(nil, issue)).to eq(true) + expect(commit1.has_been_reverted?(user, issue)).to eq(true) end it 'returns false a commit has not been reverted' do - issue = create(:issue) - - expect(commit1.has_been_reverted?(nil, issue)).to eq(false) + expect(commit1.has_been_reverted?(user, issue)).to eq(false) end end end diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index e3bb3482d6..f106c073bd 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -173,25 +173,26 @@ eos describe '#reverts_commit?' do let(:another_commit) { double(:commit, revert_description: "This reverts commit #{commit.sha}") } + let(:user) { commit.author } - it { expect(commit.reverts_commit?(another_commit)).to be_falsy } + it { expect(commit.reverts_commit?(another_commit, user)).to be_falsy } context 'commit has no description' do before { allow(commit).to receive(:description?).and_return(false) } - it { expect(commit.reverts_commit?(another_commit)).to be_falsy } + it { expect(commit.reverts_commit?(another_commit, user)).to be_falsy } end context "another_commit's description does not revert commit" do before { allow(commit).to receive(:description).and_return("Foo Bar") } - it { expect(commit.reverts_commit?(another_commit)).to be_falsy } + it { expect(commit.reverts_commit?(another_commit, user)).to be_falsy } end context "another_commit's description reverts commit" do before { allow(commit).to receive(:description).and_return("Foo #{another_commit.revert_description} Bar") } - it { expect(commit.reverts_commit?(another_commit)).to be_truthy } + it { expect(commit.reverts_commit?(another_commit, user)).to be_truthy } end context "another_commit's description reverts merged merge request" do @@ -201,7 +202,7 @@ eos allow(commit).to receive(:description).and_return("Foo #{another_commit.revert_description} Bar") end - it { expect(commit.reverts_commit?(another_commit)).to be_truthy } + it { expect(commit.reverts_commit?(another_commit, user)).to be_truthy } end end diff --git a/spec/models/cycle_analytics/code_spec.rb b/spec/models/cycle_analytics/code_spec.rb index 7691d690db..7771785ead 100644 --- a/spec/models/cycle_analytics/code_spec.rb +++ b/spec/models/cycle_analytics/code_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#code', feature: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalytics.new(project, from: from_date) } + subject { CycleAnalytics.new(project, user, from: from_date) } context 'with deployment' do generate_cycle_analytics_spec( diff --git a/spec/models/cycle_analytics/issue_spec.rb b/spec/models/cycle_analytics/issue_spec.rb index f649b44d36..5ed3d37f2f 100644 --- a/spec/models/cycle_analytics/issue_spec.rb +++ b/spec/models/cycle_analytics/issue_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#issue', models: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalytics.new(project, from: from_date) } + subject { CycleAnalytics.new(project, user, from: from_date) } generate_cycle_analytics_spec( phase: :issue, diff --git a/spec/models/cycle_analytics/plan_spec.rb b/spec/models/cycle_analytics/plan_spec.rb index 2cdefbeef2..baf3e3241a 100644 --- a/spec/models/cycle_analytics/plan_spec.rb +++ b/spec/models/cycle_analytics/plan_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#plan', feature: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalytics.new(project, from: from_date) } + subject { CycleAnalytics.new(project, user, from: from_date) } generate_cycle_analytics_spec( phase: :plan, diff --git a/spec/models/cycle_analytics/production_spec.rb b/spec/models/cycle_analytics/production_spec.rb index 1f5e5cab92..21b9c6e715 100644 --- a/spec/models/cycle_analytics/production_spec.rb +++ b/spec/models/cycle_analytics/production_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#production', feature: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalytics.new(project, from: from_date) } + subject { CycleAnalytics.new(project, user, from: from_date) } generate_cycle_analytics_spec( phase: :production, diff --git a/spec/models/cycle_analytics/review_spec.rb b/spec/models/cycle_analytics/review_spec.rb index 0ed080a42b..158621d59a 100644 --- a/spec/models/cycle_analytics/review_spec.rb +++ b/spec/models/cycle_analytics/review_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#review', feature: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalytics.new(project, from: from_date) } + subject { CycleAnalytics.new(project, user, from: from_date) } generate_cycle_analytics_spec( phase: :review, diff --git a/spec/models/cycle_analytics/staging_spec.rb b/spec/models/cycle_analytics/staging_spec.rb index af1c4477dd..dad653964b 100644 --- a/spec/models/cycle_analytics/staging_spec.rb +++ b/spec/models/cycle_analytics/staging_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#staging', feature: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalytics.new(project, from: from_date) } + subject { CycleAnalytics.new(project, user, from: from_date) } generate_cycle_analytics_spec( phase: :staging, diff --git a/spec/models/cycle_analytics/summary_spec.rb b/spec/models/cycle_analytics/summary_spec.rb index 9d67bc82cb..725bc68b25 100644 --- a/spec/models/cycle_analytics/summary_spec.rb +++ b/spec/models/cycle_analytics/summary_spec.rb @@ -4,7 +4,7 @@ describe CycleAnalytics::Summary, models: true do let(:project) { create(:project) } let(:from) { Time.now } let(:user) { create(:user, :admin) } - subject { described_class.new(project, from: from) } + subject { described_class.new(project, user, from: from) } describe "#new_issues" do it "finds the number of issues created after the 'from date'" do diff --git a/spec/models/cycle_analytics/test_spec.rb b/spec/models/cycle_analytics/test_spec.rb index 02ddfeed9c..2313724e8f 100644 --- a/spec/models/cycle_analytics/test_spec.rb +++ b/spec/models/cycle_analytics/test_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#test', feature: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalytics.new(project, from: from_date) } + subject { CycleAnalytics.new(project, user, from: from_date) } generate_cycle_analytics_spec( phase: :test, diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 75d104584f..b1b41d9305 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -22,26 +22,6 @@ describe Issue, models: true do it { is_expected.to have_db_index(:deleted_at) } end - describe '.visible_to_user' do - let(:user) { create(:user) } - let(:authorized_user) { create(:user) } - let(:project) { create(:project, namespace: authorized_user.namespace) } - let!(:public_issue) { create(:issue, project: project) } - let!(:confidential_issue) { create(:issue, project: project, confidential: true) } - - it 'returns non confidential issues for nil user' do - expect(Issue.visible_to_user(nil).count).to be(1) - end - - it 'returns non confidential issues for user not authorized for the issues projects' do - expect(Issue.visible_to_user(user).count).to be(1) - end - - it 'returns all issues for user authorized for the issues projects' do - expect(Issue.visible_to_user(authorized_user).count).to be(2) - end - end - describe '#to_reference' do it 'returns a String reference to the object' do expect(subject.to_reference).to eq "##{subject.iid}" diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 6e5137602a..8dac83ac60 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -202,7 +202,7 @@ describe MergeRequest, models: true do end end - describe "#mr_and_commit_notes" do + describe "#related_notes" do let!(:merge_request) { create(:merge_request) } before do @@ -214,7 +214,7 @@ describe MergeRequest, models: true do it "includes notes for commits" do expect(merge_request.commits).not_to be_empty - expect(merge_request.mr_and_commit_notes.count).to eq(2) + expect(merge_request.related_notes.count).to eq(2) end it "includes notes for commits from target project as well" do @@ -222,7 +222,7 @@ describe MergeRequest, models: true do project: merge_request.target_project) expect(merge_request.commits).not_to be_empty - expect(merge_request.mr_and_commit_notes.count).to eq(3) + expect(merge_request.related_notes.count).to eq(3) end end diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index 33fe22dd98..2ee4dbeb47 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -15,8 +15,9 @@ describe Milestone, models: true do it { is_expected.to validate_presence_of(:project) } end - let(:milestone) { create(:milestone) } - let(:issue) { create(:issue) } + let(:project) { create(:project, :public) } + let(:milestone) { create(:milestone, project: project) } + let(:issue) { create(:issue, project: project) } let(:user) { create(:user) } describe "#title" do @@ -101,8 +102,8 @@ describe Milestone, models: true do describe :items_count do before do - milestone.issues << create(:issue) - milestone.issues << create(:closed_issue) + milestone.issues << create(:issue, project: project) + milestone.issues << create(:closed_issue, project: project) milestone.merge_requests << create(:merge_request) end @@ -117,7 +118,7 @@ describe Milestone, models: true do describe '#total_items_count' do before do - create :closed_issue, milestone: milestone + create :closed_issue, milestone: milestone, project: project create :merge_request, milestone: milestone end diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index e6b6e7c063..c6b24c3283 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -162,44 +162,6 @@ describe Note, models: true do end end - describe '.search' do - let(:note) { create(:note_on_issue, note: 'WoW') } - - it 'returns notes with matching content' do - expect(described_class.search(note.note)).to eq([note]) - end - - it 'returns notes with matching content regardless of the casing' do - expect(described_class.search('WOW')).to eq([note]) - end - - context "confidential issues" do - let(:user) { create(:user) } - let(:project) { create(:project) } - let(:confidential_issue) { create(:issue, :confidential, project: project, author: user) } - let(:confidential_note) { create(:note, note: "Random", noteable: confidential_issue, project: confidential_issue.project) } - - it "returns notes with matching content if user can see the issue" do - expect(described_class.search(confidential_note.note, as_user: user)).to eq([confidential_note]) - end - - it "does not return notes with matching content if user can not see the issue" do - user = create(:user) - expect(described_class.search(confidential_note.note, as_user: user)).to be_empty - end - - it "does not return notes with matching content for project members with guest role" do - user = create(:user) - project.team << [user, :guest] - expect(described_class.search(confidential_note.note, as_user: user)).to be_empty - end - - it "does not return notes with matching content for unauthenticated users" do - expect(described_class.search(confidential_note.note)).to be_empty - end - end - end - describe "editable?" do it "returns true" do note = build(:note) diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 0245897938..c519ad779e 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -353,10 +353,15 @@ describe Project, models: true do describe '#get_issue' do let(:project) { create(:empty_project) } let!(:issue) { create(:issue, project: project) } + let(:user) { create(:user) } + + before do + project.team << [user, :developer] + end context 'with default issues tracker' do it 'returns an issue' do - expect(project.get_issue(issue.iid)).to eq issue + expect(project.get_issue(issue.iid, user)).to eq issue end it 'returns count of open issues' do @@ -364,7 +369,12 @@ describe Project, models: true do end it 'returns nil when no issue found' do - expect(project.get_issue(999)).to be_nil + expect(project.get_issue(999, user)).to be_nil + end + + it "returns nil when user doesn't have access" do + user = create(:user) + expect(project.get_issue(issue.iid, user)).to eq nil end end @@ -374,7 +384,7 @@ describe Project, models: true do end it 'returns an ExternalIssue' do - issue = project.get_issue('FOO-1234') + issue = project.get_issue('FOO-1234', user) expect(issue).to be_kind_of(ExternalIssue) expect(issue.iid).to eq 'FOO-1234' expect(issue.project).to eq project diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 19b3e7e470..1158ea9c96 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1275,6 +1275,32 @@ describe Repository, models: true do expect(tag).to be_a(Gitlab::Git::Tag) end + + it 'passes commit SHA to pre-receive and update hooks,\ + and tag SHA to post-receive hook' do + pre_receive_hook = Gitlab::Git::Hook.new('pre-receive', repository.path_to_repo) + update_hook = Gitlab::Git::Hook.new('update', repository.path_to_repo) + post_receive_hook = Gitlab::Git::Hook.new('post-receive', repository.path_to_repo) + + allow(Gitlab::Git::Hook).to receive(:new). + and_return(pre_receive_hook, update_hook, post_receive_hook) + + allow(pre_receive_hook).to receive(:trigger).and_call_original + allow(update_hook).to receive(:trigger).and_call_original + allow(post_receive_hook).to receive(:trigger).and_call_original + + tag = repository.add_tag(user, '8.5', 'master', 'foo') + + commit_sha = repository.commit('master').id + tag_sha = tag.target + + expect(pre_receive_hook).to have_received(:trigger). + with(anything, anything, commit_sha, anything) + expect(update_hook).to have_received(:trigger). + with(anything, anything, commit_sha, anything) + expect(post_receive_hook).to have_received(:trigger). + with(anything, anything, tag_sha, anything) + end end context 'with an invalid target' do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 65b2896930..99378cc8cb 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -599,17 +599,6 @@ describe User, models: true do end end - describe 'by_username_or_id' do - let(:user1) { create(:user, username: 'foo') } - - it "gets the correct user" do - expect(User.by_username_or_id(user1.id)).to eq(user1) - expect(User.by_username_or_id('foo')).to eq(user1) - expect(User.by_username_or_id(-1)).to be_nil - expect(User.by_username_or_id('bar')).to be_nil - end - end - describe '.find_by_ssh_key_id' do context 'using an existing SSH key ID' do let(:user) { create(:user) } diff --git a/spec/requests/api/api_helpers_spec.rb b/spec/requests/api/api_helpers_spec.rb deleted file mode 100644 index 01bb9e955e..0000000000 --- a/spec/requests/api/api_helpers_spec.rb +++ /dev/null @@ -1,293 +0,0 @@ -require 'spec_helper' - -describe API::Helpers, api: true do - include API::Helpers - include ApiHelpers - include SentryHelper - - let(:user) { create(:user) } - let(:admin) { create(:admin) } - let(:key) { create(:key, user: user) } - - let(:params) { {} } - let(:env) { { 'REQUEST_METHOD' => 'GET' } } - let(:request) { Rack::Request.new(env) } - - def set_env(token_usr, identifier) - clear_env - clear_param - env[API::Helpers::PRIVATE_TOKEN_HEADER] = token_usr.private_token - env[API::Helpers::SUDO_HEADER] = identifier - end - - def set_param(token_usr, identifier) - clear_env - clear_param - params[API::Helpers::PRIVATE_TOKEN_PARAM] = token_usr.private_token - params[API::Helpers::SUDO_PARAM] = identifier - end - - def clear_env - env.delete(API::Helpers::PRIVATE_TOKEN_HEADER) - env.delete(API::Helpers::SUDO_HEADER) - end - - def clear_param - params.delete(API::Helpers::PRIVATE_TOKEN_PARAM) - params.delete(API::Helpers::SUDO_PARAM) - end - - def warden_authenticate_returns(value) - warden = double("warden", authenticate: value) - env['warden'] = warden - end - - def doorkeeper_guard_returns(value) - allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ value } - end - - def error!(message, status) - raise Exception - end - - describe ".current_user" do - subject { current_user } - - describe "Warden authentication" do - before { doorkeeper_guard_returns false } - - context "with invalid credentials" do - context "GET request" do - before { env['REQUEST_METHOD'] = 'GET' } - it { is_expected.to be_nil } - end - end - - context "with valid credentials" do - before { warden_authenticate_returns user } - - context "GET request" do - before { env['REQUEST_METHOD'] = 'GET' } - it { is_expected.to eq(user) } - end - - context "HEAD request" do - before { env['REQUEST_METHOD'] = 'HEAD' } - it { is_expected.to eq(user) } - end - - context "PUT request" do - before { env['REQUEST_METHOD'] = 'PUT' } - it { is_expected.to be_nil } - end - - context "POST request" do - before { env['REQUEST_METHOD'] = 'POST' } - it { is_expected.to be_nil } - end - - context "DELETE request" do - before { env['REQUEST_METHOD'] = 'DELETE' } - it { is_expected.to be_nil } - end - end - end - - describe "when authenticating using a user's private token" do - it "returns nil for an invalid token" do - env[API::Helpers::PRIVATE_TOKEN_HEADER] = 'invalid token' - allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false } - expect(current_user).to be_nil - end - - it "returns nil for a user without access" do - env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token - allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false) - expect(current_user).to be_nil - end - - it "leaves user as is when sudo not specified" do - env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token - expect(current_user).to eq(user) - clear_env - params[API::Helpers::PRIVATE_TOKEN_PARAM] = user.private_token - expect(current_user).to eq(user) - end - end - - describe "when authenticating using a user's personal access tokens" do - let(:personal_access_token) { create(:personal_access_token, user: user) } - - it "returns nil for an invalid token" do - env[API::Helpers::PRIVATE_TOKEN_HEADER] = 'invalid token' - allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false } - expect(current_user).to be_nil - end - - it "returns nil for a user without access" do - env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token - allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false) - expect(current_user).to be_nil - end - - it "leaves user as is when sudo not specified" do - env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token - expect(current_user).to eq(user) - clear_env - params[API::Helpers::PRIVATE_TOKEN_PARAM] = personal_access_token.token - expect(current_user).to eq(user) - end - - it 'does not allow revoked tokens' do - personal_access_token.revoke! - env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token - allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false } - expect(current_user).to be_nil - end - - it 'does not allow expired tokens' do - personal_access_token.update_attributes!(expires_at: 1.day.ago) - env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token - allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false } - expect(current_user).to be_nil - end - end - - it "changes current user to sudo when admin" do - set_env(admin, user.id) - expect(current_user).to eq(user) - set_param(admin, user.id) - expect(current_user).to eq(user) - set_env(admin, user.username) - expect(current_user).to eq(user) - set_param(admin, user.username) - expect(current_user).to eq(user) - end - - it "throws an error when the current user is not an admin and attempting to sudo" do - set_env(user, admin.id) - expect { current_user }.to raise_error(Exception) - set_param(user, admin.id) - expect { current_user }.to raise_error(Exception) - set_env(user, admin.username) - expect { current_user }.to raise_error(Exception) - set_param(user, admin.username) - expect { current_user }.to raise_error(Exception) - end - - it "throws an error when the user cannot be found for a given id" do - id = user.id + admin.id - expect(user.id).not_to eq(id) - expect(admin.id).not_to eq(id) - set_env(admin, id) - expect { current_user }.to raise_error(Exception) - - set_param(admin, id) - expect { current_user }.to raise_error(Exception) - end - - it "throws an error when the user cannot be found for a given username" do - username = "#{user.username}#{admin.username}" - expect(user.username).not_to eq(username) - expect(admin.username).not_to eq(username) - set_env(admin, username) - expect { current_user }.to raise_error(Exception) - - set_param(admin, username) - expect { current_user }.to raise_error(Exception) - end - - it "handles sudo's to oneself" do - set_env(admin, admin.id) - expect(current_user).to eq(admin) - set_param(admin, admin.id) - expect(current_user).to eq(admin) - set_env(admin, admin.username) - expect(current_user).to eq(admin) - set_param(admin, admin.username) - expect(current_user).to eq(admin) - end - - it "handles multiple sudo's to oneself" do - set_env(admin, user.id) - expect(current_user).to eq(user) - expect(current_user).to eq(user) - set_env(admin, user.username) - expect(current_user).to eq(user) - expect(current_user).to eq(user) - - set_param(admin, user.id) - expect(current_user).to eq(user) - expect(current_user).to eq(user) - set_param(admin, user.username) - expect(current_user).to eq(user) - expect(current_user).to eq(user) - end - - it "handles multiple sudo's to oneself using string ids" do - set_env(admin, user.id.to_s) - expect(current_user).to eq(user) - expect(current_user).to eq(user) - - set_param(admin, user.id.to_s) - expect(current_user).to eq(user) - expect(current_user).to eq(user) - end - end - - describe '.sudo_identifier' do - it "returns integers when input is an int" do - set_env(admin, '123') - expect(sudo_identifier).to eq(123) - set_env(admin, '0001234567890') - expect(sudo_identifier).to eq(1234567890) - - set_param(admin, '123') - expect(sudo_identifier).to eq(123) - set_param(admin, '0001234567890') - expect(sudo_identifier).to eq(1234567890) - end - - it "returns string when input is an is not an int" do - set_env(admin, '12.30') - expect(sudo_identifier).to eq("12.30") - set_env(admin, 'hello') - expect(sudo_identifier).to eq('hello') - set_env(admin, ' 123') - expect(sudo_identifier).to eq(' 123') - - set_param(admin, '12.30') - expect(sudo_identifier).to eq("12.30") - set_param(admin, 'hello') - expect(sudo_identifier).to eq('hello') - set_param(admin, ' 123') - expect(sudo_identifier).to eq(' 123') - end - end - - describe '.handle_api_exception' do - before do - allow_any_instance_of(self.class).to receive(:sentry_enabled?).and_return(true) - allow_any_instance_of(self.class).to receive(:rack_response) - end - - it 'does not report a MethodNotAllowed exception to Sentry' do - exception = Grape::Exceptions::MethodNotAllowed.new({ 'X-GitLab-Test' => '1' }) - allow(exception).to receive(:backtrace).and_return(caller) - - expect(Raven).not_to receive(:capture_exception).with(exception) - - handle_api_exception(exception) - end - - it 'does report RuntimeError to Sentry' do - exception = RuntimeError.new('test error') - allow(exception).to receive(:backtrace).and_return(caller) - - expect_any_instance_of(self.class).to receive(:sentry_context) - expect(Raven).to receive(:capture_exception).with(exception) - - handle_api_exception(exception) - end - end -end diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb new file mode 100644 index 0000000000..ca104bd73a --- /dev/null +++ b/spec/requests/api/helpers_spec.rb @@ -0,0 +1,373 @@ +require 'spec_helper' + +describe API::Helpers, api: true do + include API::Helpers + include SentryHelper + + let(:user) { create(:user) } + let(:admin) { create(:admin) } + let(:key) { create(:key, user: user) } + + let(:params) { {} } + let(:env) { { 'REQUEST_METHOD' => 'GET' } } + let(:request) { Rack::Request.new(env) } + + def set_env(user_or_token, identifier) + clear_env + clear_param + env[API::Helpers::PRIVATE_TOKEN_HEADER] = user_or_token.respond_to?(:private_token) ? user_or_token.private_token : user_or_token + env[API::Helpers::SUDO_HEADER] = identifier.to_s + end + + def set_param(user_or_token, identifier) + clear_env + clear_param + params[API::Helpers::PRIVATE_TOKEN_PARAM] = user_or_token.respond_to?(:private_token) ? user_or_token.private_token : user_or_token + params[API::Helpers::SUDO_PARAM] = identifier.to_s + end + + def clear_env + env.delete(API::Helpers::PRIVATE_TOKEN_HEADER) + env.delete(API::Helpers::SUDO_HEADER) + end + + def clear_param + params.delete(API::Helpers::PRIVATE_TOKEN_PARAM) + params.delete(API::Helpers::SUDO_PARAM) + end + + def warden_authenticate_returns(value) + warden = double("warden", authenticate: value) + env['warden'] = warden + end + + def doorkeeper_guard_returns(value) + allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ value } + end + + def error!(message, status) + raise Exception.new("#{status} - #{message}") + end + + describe ".current_user" do + subject { current_user } + + describe "Warden authentication" do + before { doorkeeper_guard_returns false } + + context "with invalid credentials" do + context "GET request" do + before { env['REQUEST_METHOD'] = 'GET' } + it { is_expected.to be_nil } + end + end + + context "with valid credentials" do + before { warden_authenticate_returns user } + + context "GET request" do + before { env['REQUEST_METHOD'] = 'GET' } + it { is_expected.to eq(user) } + end + + context "HEAD request" do + before { env['REQUEST_METHOD'] = 'HEAD' } + it { is_expected.to eq(user) } + end + + context "PUT request" do + before { env['REQUEST_METHOD'] = 'PUT' } + it { is_expected.to be_nil } + end + + context "POST request" do + before { env['REQUEST_METHOD'] = 'POST' } + it { is_expected.to be_nil } + end + + context "DELETE request" do + before { env['REQUEST_METHOD'] = 'DELETE' } + it { is_expected.to be_nil } + end + end + end + + describe "when authenticating using a user's private token" do + it "returns nil for an invalid token" do + env[API::Helpers::PRIVATE_TOKEN_HEADER] = 'invalid token' + allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false } + expect(current_user).to be_nil + end + + it "returns nil for a user without access" do + env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token + allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false) + expect(current_user).to be_nil + end + + it "leaves user as is when sudo not specified" do + env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token + expect(current_user).to eq(user) + clear_env + params[API::Helpers::PRIVATE_TOKEN_PARAM] = user.private_token + expect(current_user).to eq(user) + end + end + + describe "when authenticating using a user's personal access tokens" do + let(:personal_access_token) { create(:personal_access_token, user: user) } + + it "returns nil for an invalid token" do + env[API::Helpers::PRIVATE_TOKEN_HEADER] = 'invalid token' + allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false } + expect(current_user).to be_nil + end + + it "returns nil for a user without access" do + env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token + allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false) + expect(current_user).to be_nil + end + + it "leaves user as is when sudo not specified" do + env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token + expect(current_user).to eq(user) + clear_env + params[API::Helpers::PRIVATE_TOKEN_PARAM] = personal_access_token.token + expect(current_user).to eq(user) + end + + it 'does not allow revoked tokens' do + personal_access_token.revoke! + env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token + allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false } + expect(current_user).to be_nil + end + + it 'does not allow expired tokens' do + personal_access_token.update_attributes!(expires_at: 1.day.ago) + env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token + allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false } + expect(current_user).to be_nil + end + end + + context 'sudo usage' do + context 'with admin' do + context 'with header' do + context 'with id' do + it 'changes current_user to sudo' do + set_env(admin, user.id) + + expect(current_user).to eq(user) + end + + it 'memoize the current_user: sudo permissions are not run against the sudoed user' do + set_env(admin, user.id) + + expect(current_user).to eq(user) + expect(current_user).to eq(user) + end + + it 'handles sudo to oneself' do + set_env(admin, admin.id) + + expect(current_user).to eq(admin) + end + + it 'throws an error when user cannot be found' do + id = user.id + admin.id + expect(user.id).not_to eq(id) + expect(admin.id).not_to eq(id) + + set_env(admin, id) + + expect { current_user }.to raise_error(Exception) + end + end + + context 'with username' do + it 'changes current_user to sudo' do + set_env(admin, user.username) + + expect(current_user).to eq(user) + end + + it 'handles sudo to oneself' do + set_env(admin, admin.username) + + expect(current_user).to eq(admin) + end + + it "throws an error when the user cannot be found for a given username" do + username = "#{user.username}#{admin.username}" + expect(user.username).not_to eq(username) + expect(admin.username).not_to eq(username) + + set_env(admin, username) + + expect { current_user }.to raise_error(Exception) + end + end + end + + context 'with param' do + context 'with id' do + it 'changes current_user to sudo' do + set_param(admin, user.id) + + expect(current_user).to eq(user) + end + + it 'handles sudo to oneself' do + set_param(admin, admin.id) + + expect(current_user).to eq(admin) + end + + it 'handles sudo to oneself using string' do + set_env(admin, user.id.to_s) + + expect(current_user).to eq(user) + end + + it 'throws an error when user cannot be found' do + id = user.id + admin.id + expect(user.id).not_to eq(id) + expect(admin.id).not_to eq(id) + + set_param(admin, id) + + expect { current_user }.to raise_error(Exception) + end + end + + context 'with username' do + it 'changes current_user to sudo' do + set_param(admin, user.username) + + expect(current_user).to eq(user) + end + + it 'handles sudo to oneself' do + set_param(admin, admin.username) + + expect(current_user).to eq(admin) + end + + it "throws an error when the user cannot be found for a given username" do + username = "#{user.username}#{admin.username}" + expect(user.username).not_to eq(username) + expect(admin.username).not_to eq(username) + + set_param(admin, username) + + expect { current_user }.to raise_error(Exception) + end + end + end + end + + context 'with regular user' do + context 'with env' do + it 'changes current_user to sudo when admin and user id' do + set_env(user, admin.id) + + expect { current_user }.to raise_error(Exception) + end + + it 'changes current_user to sudo when admin and user username' do + set_env(user, admin.username) + + expect { current_user }.to raise_error(Exception) + end + end + + context 'with params' do + it 'changes current_user to sudo when admin and user id' do + set_param(user, admin.id) + + expect { current_user }.to raise_error(Exception) + end + + it 'changes current_user to sudo when admin and user username' do + set_param(user, admin.username) + + expect { current_user }.to raise_error(Exception) + end + end + end + end + end + + describe '.sudo?' do + context 'when no sudo env or param is passed' do + before do + doorkeeper_guard_returns(nil) + end + + it 'returns false' do + expect(sudo?).to be_falsy + end + end + + context 'when sudo env or param is passed', 'user is not an admin' do + before do + set_env(user, '123') + end + + it 'returns an 403 Forbidden' do + expect { sudo? }.to raise_error '403 - {"message"=>"403 Forbidden - Must be admin to use sudo"}' + end + end + + context 'when sudo env or param is passed', 'user is admin' do + context 'personal access token is used' do + before do + personal_access_token = create(:personal_access_token, user: admin) + set_env(personal_access_token.token, user.id) + end + + it 'returns an 403 Forbidden' do + expect { sudo? }.to raise_error '403 - {"message"=>"403 Forbidden - Private token must be specified in order to use sudo"}' + end + end + + context 'private access token is used' do + before do + set_env(admin.private_token, user.id) + end + + it 'returns true' do + expect(sudo?).to be_truthy + end + end + end + end + + describe '.handle_api_exception' do + before do + allow_any_instance_of(self.class).to receive(:sentry_enabled?).and_return(true) + allow_any_instance_of(self.class).to receive(:rack_response) + end + + it 'does not report a MethodNotAllowed exception to Sentry' do + exception = Grape::Exceptions::MethodNotAllowed.new({ 'X-GitLab-Test' => '1' }) + allow(exception).to receive(:backtrace).and_return(caller) + + expect(Raven).not_to receive(:capture_exception).with(exception) + + handle_api_exception(exception) + end + + it 'does report RuntimeError to Sentry' do + exception = RuntimeError.new('test error') + allow(exception).to receive(:backtrace).and_return(caller) + + expect_any_instance_of(self.class).to receive(:sentry_context) + expect(Raven).to receive(:capture_exception).with(exception) + + handle_api_exception(exception) + end + end +end diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index beed53d1e5..b9146f7840 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -365,6 +365,24 @@ describe API::API, api: true do let(:base_url) { "/projects/#{project.id}" } let(:title) { milestone.title } + it "returns 404 on private projects for other users" do + private_project = create(:empty_project, :private) + create(:issue, project: private_project) + + get api("/projects/#{private_project.id}/issues", non_member) + + expect(response).to have_http_status(404) + end + + it 'returns no issues when user has access to project but not issues' do + restricted_project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE) + create(:issue, project: restricted_project) + + get api("/projects/#{restricted_project.id}/issues", non_member) + + expect(json_response).to eq([]) + end + it 'returns project issues without confidential issues for non project members' do get api("#{base_url}/issues", non_member) expect(response).to have_http_status(200) @@ -697,6 +715,14 @@ describe API::API, api: true do expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time) end end + + context 'the user can only read the issue' do + it 'cannot create new labels' do + expect do + post api("/projects/#{project.id}/issues", non_member), title: 'new issue', labels: 'label, label2' + end.not_to change { project.labels.count } + end + end end describe 'POST /projects/:id/issues with spam filtering' do @@ -839,8 +865,8 @@ describe API::API, api: true do end it 'removes all labels' do - put api("/projects/#{project.id}/issues/#{issue.id}", user), - labels: '' + put api("/projects/#{project.id}/issues/#{issue.id}", user), labels: '' + expect(response).to have_http_status(200) expect(json_response['labels']).to eq([]) end @@ -892,8 +918,8 @@ describe API::API, api: true do update_time = 2.weeks.ago put api("/projects/#{project.id}/issues/#{issue.id}", user), labels: 'label3', state_event: 'close', updated_at: update_time - expect(response).to have_http_status(200) + expect(response).to have_http_status(200) expect(json_response['labels']).to include 'label3' expect(Time.parse(json_response['updated_at'])).to be_like_time(update_time) end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index b813ee967f..d5303dfc13 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -392,14 +392,6 @@ describe API::API, api: true do end end - describe "PUT /projects/:id/merge_requests/:merge_request_id to close MR" do - it "returns merge_request" do - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: "close" - expect(response).to have_http_status(200) - expect(json_response['state']).to eq('closed') - end - end - describe "PUT /projects/:id/merge_requests/:merge_request_id/merge" do let(:pipeline) { create(:ci_pipeline_without_jobs) } @@ -476,6 +468,15 @@ describe API::API, api: true do end describe "PUT /projects/:id/merge_requests/:merge_request_id" do + context "to close a MR" do + it "returns merge_request" do + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: "close" + + expect(response).to have_http_status(200) + expect(json_response['state']).to eq('closed') + end + end + it "updates title and returns merge_request" do put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), title: "New title" expect(response).to have_http_status(200) @@ -496,7 +497,7 @@ describe API::API, api: true do it "returns 400 when source_branch is specified" do put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), - source_branch: "master", target_branch: "master" + source_branch: "master", target_branch: "master" expect(response).to have_http_status(400) end @@ -507,10 +508,10 @@ describe API::API, api: true do end it 'allows special label names' do - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", - user), - title: 'new issue', - labels: 'label, label?, label&foo, ?, &' + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), + title: 'new issue', + labels: 'label, label?, label&foo, ?, &' + expect(response.status).to eq(200) expect(json_response['labels']).to include 'label' expect(json_response['labels']).to include 'label?' @@ -539,7 +540,7 @@ describe API::API, api: true do it "returns 404 if note is attached to non existent merge request" do post api("/projects/#{project.id}/merge_requests/404/comments", user), - note: 'My comment' + note: 'My comment' expect(response).to have_http_status(404) end end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 2c4e73ed57..b93510fccc 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -615,20 +615,78 @@ describe API::API, api: true do end describe "GET /user" do - it "returns current user" do - get api("/user", user) - expect(response).to have_http_status(200) - expect(json_response['email']).to eq(user.email) - expect(json_response['is_admin']).to eq(user.is_admin?) - expect(json_response['can_create_project']).to eq(user.can_create_project?) - expect(json_response['can_create_group']).to eq(user.can_create_group?) - expect(json_response['projects_limit']).to eq(user.projects_limit) - expect(json_response['private_token']).to be_blank + let(:personal_access_token) { create(:personal_access_token, user: user).token } + + context 'with regular user' do + context 'with personal access token' do + it 'returns 403 without private token when sudo is defined' do + get api("/user?private_token=#{personal_access_token}&sudo=123") + + expect(response).to have_http_status(403) + end + end + + context 'with private token' do + it 'returns 403 without private token when sudo defined' do + get api("/user?private_token=#{user.private_token}&sudo=123") + + expect(response).to have_http_status(403) + end + end + + it 'returns current user without private token when sudo not defined' do + get api("/user", user) + + expect(response).to have_http_status(200) + expect(response).to match_response_schema('user/public') + expect(json_response['id']).to eq(user.id) + end end - it "returns 401 error if user is unauthenticated" do - get api("/user") - expect(response).to have_http_status(401) + context 'with admin' do + let(:admin_personal_access_token) { create(:personal_access_token, user: admin).token } + + context 'with personal access token' do + it 'returns 403 without private token when sudo defined' do + get api("/user?private_token=#{admin_personal_access_token}&sudo=#{user.id}") + + expect(response).to have_http_status(403) + end + + it 'returns initial current user without private token when sudo not defined' do + get api("/user?private_token=#{admin_personal_access_token}") + + expect(response).to have_http_status(200) + expect(response).to match_response_schema('user/public') + expect(json_response['id']).to eq(admin.id) + end + end + + context 'with private token' do + it 'returns sudoed user with private token when sudo defined' do + get api("/user?private_token=#{admin.private_token}&sudo=#{user.id}") + + expect(response).to have_http_status(200) + expect(response).to match_response_schema('user/login') + expect(json_response['id']).to eq(user.id) + end + + it 'returns initial current user without private token when sudo not defined' do + get api("/user?private_token=#{admin.private_token}") + + expect(response).to have_http_status(200) + expect(response).to match_response_schema('user/public') + expect(json_response['id']).to eq(admin.id) + end + end + end + + context 'with unauthenticated user' do + it "returns 401 error if user is unauthenticated" do + get api("/user") + + expect(response).to have_http_status(401) + end end end diff --git a/spec/services/labels/transfer_service_spec.rb b/spec/services/labels/transfer_service_spec.rb index ddf3527dc0..13654a0881 100644 --- a/spec/services/labels/transfer_service_spec.rb +++ b/spec/services/labels/transfer_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Labels::TransferService, services: true do describe '#execute' do - let(:user) { create(:user) } + let(:user) { create(:admin) } let(:group_1) { create(:group) } let(:group_2) { create(:group) } let(:group_3) { create(:group) } diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb index 3a3f07ddcb..2d3a945337 100644 --- a/spec/services/merge_requests/build_service_spec.rb +++ b/spec/services/merge_requests/build_service_spec.rb @@ -24,6 +24,8 @@ describe MergeRequests::BuildService, services: true do end before do + project.team << [user, :guest] + allow(CompareService).to receive_message_chain(:new, :execute).and_return(compare) end @@ -166,6 +168,16 @@ describe MergeRequests::BuildService, services: true do expect(merge_request.title).to eq("Resolve \"#{issue.title}\"") end + context 'when issue is not accessible to user' do + before do + project.team.truncate + end + + it 'uses branch title as the merge request title' do + expect(merge_request.title).to eq("#{issue.iid} fix issue") + end + end + context 'issue does not exist' do let(:source_branch) { "#{issue.iid.succ}-fix-issue" } diff --git a/vendor/assets/javascripts/jquery.turbolinks.js b/vendor/assets/javascripts/jquery.turbolinks.js new file mode 100644 index 0000000000..fd6e95e75d --- /dev/null +++ b/vendor/assets/javascripts/jquery.turbolinks.js @@ -0,0 +1,49 @@ +// Generated by CoffeeScript 1.7.1 + +/* +jQuery.Turbolinks ~ https://github.com/kossnocorp/jquery.turbolinks +jQuery plugin for drop-in fix binded events problem caused by Turbolinks + +The MIT License +Copyright (c) 2012-2013 Sasha Koss & Rico Sta. Cruz + */ + +(function() { + var $, $document; + + $ = window.jQuery || (typeof require === "function" ? require('jquery') : void 0); + + $document = $(document); + + $.turbo = { + version: '2.1.0', + isReady: false, + use: function(load, fetch) { + return $document.off('.turbo').on("" + load + ".turbo", this.onLoad).on("" + fetch + ".turbo", this.onFetch); + }, + addCallback: function(callback) { + if ($.turbo.isReady) { + callback($); + } + return $document.on('turbo:ready', function() { + return callback($); + }); + }, + onLoad: function() { + $.turbo.isReady = true; + return $document.trigger('turbo:ready'); + }, + onFetch: function() { + return $.turbo.isReady = false; + }, + register: function() { + $(this.onLoad); + return $.fn.ready = this.addCallback; + } + }; + + $.turbo.register(); + + $.turbo.use('page:load', 'page:fetch'); + +}).call(this);