diff options
189 files changed, 4708 insertions, 2451 deletions
diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index c28f6e151a0..9974e135022 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -196,6 +196,7 @@ window.Build = (function () { }) .done((log) => { gl.utils.setCiStatusFavicon(`${this.pageUrl}/status.json`); + if (log.state) { this.state = log.state; } @@ -220,7 +221,11 @@ window.Build = (function () { } if (!log.complete) { - this.toggleScrollAnimation(true); + if (!this.hasBeenScrolled) { + this.toggleScrollAnimation(true); + } else { + this.toggleScrollAnimation(false); + } Build.timeout = setTimeout(() => { //eslint-disable-next-line diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 31a86090242..4247540de22 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -209,8 +209,8 @@ import initExperimentalFlags from './experimental_flags'; new MilestoneSelect(); new gl.IssuableTemplateSelectors(); break; - case 'projects:merge_requests:new': - case 'projects:merge_requests:new_diffs': + case 'projects:merge_requests:creations:new': + case 'projects:merge_requests:creations:diffs': case 'projects:merge_requests:edit': new gl.Diff(); shortcut_handler = new ShortcutsNavigation(); @@ -247,10 +247,6 @@ import initExperimentalFlags from './experimental_flags'; shortcut_handler = new ShortcutsIssuable(true); new ZenMode(); break; - case "projects:merge_requests:diffs": - new gl.Diff(); - new ZenMode(); - break; case 'dashboard:activity': new gl.Activities(); break; @@ -319,7 +315,7 @@ import initExperimentalFlags from './experimental_flags'; new gl.Members(); new UsersSelect(); break; - case 'projects:members:show': + case 'projects:settings:members:show': new gl.MemberExpirationDate('.js-access-expiration-date-groups'); new GroupsSelect(); new gl.MemberExpirationDate(); @@ -386,7 +382,7 @@ import initExperimentalFlags from './experimental_flags'; case 'search:show': new Search(); break; - case 'projects:repository:show': + case 'projects:settings:repository:show': // Initialize Protected Branch Settings new gl.ProtectedBranchCreate(); new gl.ProtectedBranchEditList(); @@ -396,7 +392,7 @@ import initExperimentalFlags from './experimental_flags'; // Initialize expandable settings panels initSettingsPanels(); break; - case 'projects:ci_cd:show': + case 'projects:settings:ci_cd:show': new gl.ProjectVariables(); break; case 'ci:lints:create': diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 786b6014dc6..3cf3233cc65 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -168,9 +168,8 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; // Activate a tab based on the current action activateTab(action) { - const activate = action === 'show' ? 'notes' : action; // important note: the .tab('show') method triggers 'shown.bs.tab' event itself - $(`.merge-request-tabs a[data-action='${activate}']`).tab('show'); + $(`.merge-request-tabs a[data-action='${action}']`).tab('show'); } // Replaces the current Merge Request-specific action in the URL with a new one @@ -185,7 +184,7 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; // location.pathname # => "/namespace/project/merge_requests/1/diffs" // // location.pathname # => "/namespace/project/merge_requests/1/diffs" - // setCurrentAction('notes') + // setCurrentAction('show') // location.pathname # => "/namespace/project/merge_requests/1" // // location.pathname # => "/namespace/project/merge_requests/1/diffs" @@ -194,13 +193,13 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; // // Returns the new URL String setCurrentAction(action) { - this.currentAction = action === 'show' ? 'notes' : action; + this.currentAction = action; - // Remove a trailing '/commits' '/diffs' '/pipelines' '/new' '/new/diffs' - let newState = location.pathname.replace(/\/(commits|diffs|pipelines|new|new\/diffs)(\.html)?\/?$/, ''); + // Remove a trailing '/commits' '/diffs' '/pipelines' + let newState = location.pathname.replace(/\/(commits|diffs|pipelines)(\.html)?\/?$/, ''); // Append the new action if we're on a tab other than 'notes' - if (this.currentAction !== 'notes') { + if (this.currentAction !== 'show' && this.currentAction !== 'new') { newState += `/${this.currentAction}`; } diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 7eee0a71c66..9cff99b839c 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -147,10 +147,9 @@ top: 35px; left: 10px; bottom: 0; - overflow-y: scroll; - overflow-x: hidden; padding: 10px 20px 20px 5px; - white-space: pre; + white-space: pre-wrap; + overflow: auto; } .environment-information { @@ -399,6 +398,7 @@ .build-light-text { color: $gl-text-color-secondary; + word-wrap: break-word; } .build-gutter-toggle { diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb index 1a9904bbe57..f87db4d9e84 100644 --- a/app/controllers/concerns/creates_commit.rb +++ b/app/controllers/concerns/creates_commit.rb @@ -78,7 +78,7 @@ module CreatesCommit end def new_merge_request_path - new_namespace_project_merge_request_path( + namespace_project_new_merge_request_path( @project_to_commit_into.namespace, @project_to_commit_into, merge_request: { diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index dfc6baa34a4..5b2de93c168 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -227,7 +227,7 @@ class Projects::IssuesController < Projects::ApplicationController def issue return @issue if defined?(@issue) # The Sortable default scope causes performance issues when used with find_by - @noteable = @issue ||= @project.issues.where(iid: params[:id]).reorder(nil).take! + @noteable = @issue ||= @project.issues.find_by!(iid: params[:id]) return render_404 unless can?(current_user, :read_issue, @issue) diff --git a/app/controllers/projects/merge_requests/application_controller.rb b/app/controllers/projects/merge_requests/application_controller.rb new file mode 100644 index 00000000000..5de0f828010 --- /dev/null +++ b/app/controllers/projects/merge_requests/application_controller.rb @@ -0,0 +1,48 @@ +class Projects::MergeRequests::ApplicationController < Projects::ApplicationController + before_action :check_merge_requests_available! + before_action :merge_request + before_action :authorize_read_merge_request! + before_action :ensure_ref_fetched + + private + + def merge_request + @issuable = @merge_request ||= @project.merge_requests.find_by!(iid: params[:id]) + end + + # Make sure merge requests created before 8.0 + # have head file in refs/merge-requests/ + def ensure_ref_fetched + @merge_request.ensure_ref_fetched + end + + def merge_request_params + params.require(:merge_request) + .permit(merge_request_params_attributes) + end + + def merge_request_params_attributes + [ + :assignee_id, + :description, + :force_remove_source_branch, + :lock_version, + :milestone_id, + :source_branch, + :source_project_id, + :state_event, + :target_branch, + :target_project_id, + :task_num, + :title, + + label_ids: [] + ] + end + + def set_pipeline_variables + @pipelines = @merge_request.all_pipelines + @pipeline = @merge_request.head_pipeline + @statuses_count = @pipeline.present? ? @pipeline.statuses.relevant.count : 0 + end +end diff --git a/app/controllers/projects/merge_requests/conflicts_controller.rb b/app/controllers/projects/merge_requests/conflicts_controller.rb new file mode 100644 index 00000000000..a71f23e790d --- /dev/null +++ b/app/controllers/projects/merge_requests/conflicts_controller.rb @@ -0,0 +1,66 @@ +class Projects::MergeRequests::ConflictsController < Projects::MergeRequests::ApplicationController + include IssuableActions + + before_action :authorize_can_resolve_conflicts! + + def show + respond_to do |format| + format.html do + labels + end + + format.json do + if @conflicts_list.can_be_resolved_in_ui? + render json: @conflicts_list + elsif @merge_request.can_be_merged? + render json: { + message: 'The merge conflicts for this merge request have already been resolved. Please return to the merge request.', + type: 'error' + } + else + render json: { + message: 'The merge conflicts for this merge request cannot be resolved through GitLab. Please try to resolve them locally.', + type: 'error' + } + end + end + end + end + + def conflict_for_path + return render_404 unless @conflicts_list.can_be_resolved_in_ui? + + file = @conflicts_list.file_for_path(params[:old_path], params[:new_path]) + + return render_404 unless file + + render json: file, full_content: true + end + + def resolve_conflicts + return render_404 unless @conflicts_list.can_be_resolved_in_ui? + + if @merge_request.can_be_merged? + render status: :bad_request, json: { message: 'The merge conflicts for this merge request have already been resolved.' } + return + end + + begin + ::MergeRequests::Conflicts::ResolveService + .new(merge_request) + .execute(current_user, params) + + flash[:notice] = 'All merge conflicts were resolved. The merge request can now be merged.' + + render json: { redirect_to: namespace_project_merge_request_url(@project.namespace, @project, @merge_request, resolved_conflicts: true) } + rescue Gitlab::Conflict::ResolutionError => e + render status: :bad_request, json: { message: e.message } + end + end + + def authorize_can_resolve_conflicts! + @conflicts_list = ::MergeRequests::Conflicts::ListService.new(@merge_request) + + return render_404 unless @conflicts_list.can_be_resolved_by?(current_user) + end +end diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb new file mode 100644 index 00000000000..da058da795e --- /dev/null +++ b/app/controllers/projects/merge_requests/creations_controller.rb @@ -0,0 +1,128 @@ +class Projects::MergeRequests::CreationsController < Projects::MergeRequests::ApplicationController + include DiffForPath + include DiffHelper + + skip_before_action :merge_request + skip_before_action :ensure_ref_fetched + before_action :authorize_create_merge_request! + before_action :apply_diff_view_cookie!, only: [:diffs, :diff_for_path] + before_action :build_merge_request, except: [:create] + + def new + define_new_vars + end + + def create + @target_branches ||= [] + @merge_request = ::MergeRequests::CreateService.new(project, current_user, merge_request_params).execute + + if @merge_request.valid? + redirect_to(merge_request_path(@merge_request)) + else + @source_project = @merge_request.source_project + @target_project = @merge_request.target_project + + define_new_vars + render action: "new" + end + end + + def pipelines + @pipelines = @merge_request.all_pipelines + + Gitlab::PollingInterval.set_header(response, interval: 10_000) + + render json: { + pipelines: PipelineSerializer + .new(project: @project, current_user: @current_user) + .represent(@pipelines) + } + end + + def diffs + @diffs = if @merge_request.can_be_created + @merge_request.diffs(diff_options) + else + [] + end + @diff_notes_disabled = true + + @environment = @merge_request.environments_for(current_user).last + + render json: { html: view_to_html_string('projects/merge_requests/creations/_diffs', diffs: @diffs, environment: @environment) } + end + + def diff_for_path + @diffs = @merge_request.diffs(diff_options) + @diff_notes_disabled = true + + render_diff_for_path(@diffs) + end + + def branch_from + # This is always source + @source_project = @merge_request.nil? ? @project : @merge_request.source_project + + if params[:ref].present? + @ref = params[:ref] + @commit = @repository.commit("refs/heads/#{@ref}") + end + + render layout: false + end + + def branch_to + @target_project = selected_target_project + + if params[:ref].present? + @ref = params[:ref] + @commit = @target_project.commit("refs/heads/#{@ref}") + end + + render layout: false + end + + def update_branches + @target_project = selected_target_project + @target_branches = @target_project.repository.branch_names + + render layout: false + end + + private + + def build_merge_request + params[:merge_request] ||= ActionController::Parameters.new(source_project: @project) + @merge_request = ::MergeRequests::BuildService.new(project, current_user, merge_request_params.merge(diff_options: diff_options)).execute + end + + def define_new_vars + @noteable = @merge_request + + @target_branches = if @merge_request.target_project + @merge_request.target_project.repository.branch_names + else + [] + end + + @target_project = @merge_request.target_project + @source_project = @merge_request.source_project + @commits = @merge_request.compare_commits.reverse + @commit = @merge_request.diff_head_commit + + @note_counts = Note.where(commit_id: @commits.map(&:id)) + .group(:commit_id).count + + @labels = LabelsFinder.new(current_user, project_id: @project.id).execute + + set_pipeline_variables + end + + def selected_target_project + if @project.id.to_s == params[:target_project_id] || @project.forked_project_link.nil? + @project + else + @project.forked_project_link.forked_from_project + end + end +end diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb new file mode 100644 index 00000000000..330b7df4541 --- /dev/null +++ b/app/controllers/projects/merge_requests/diffs_controller.rb @@ -0,0 +1,66 @@ +class Projects::MergeRequests::DiffsController < Projects::MergeRequests::ApplicationController + include DiffForPath + include DiffHelper + include RendersNotes + + before_action :apply_diff_view_cookie! + before_action :define_diff_vars + before_action :define_diff_comment_vars + + def show + @environment = @merge_request.environments_for(current_user).last + + render json: { html: view_to_html_string("projects/merge_requests/diffs/_diffs") } + end + + def diff_for_path + render_diff_for_path(@diffs) + end + + private + + def define_diff_vars + @merge_request_diff = + if params[:diff_id] + @merge_request.merge_request_diffs.viewable.find(params[:diff_id]) + else + @merge_request.merge_request_diff + end + + @merge_request_diffs = @merge_request.merge_request_diffs.viewable.select_without_diff + @comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id } + + if params[:start_sha].present? + @start_sha = params[:start_sha] + @start_version = @comparable_diffs.find { |diff| diff.head_commit_sha == @start_sha } + + unless @start_version + @start_sha = @merge_request_diff.head_commit_sha + @start_version = @merge_request_diff + end + end + + @compare = + if @start_sha + @merge_request_diff.compare_with(@start_sha) + else + @merge_request_diff + end + + @diffs = @compare.diffs(diff_options) + end + + def define_diff_comment_vars + @new_diff_note_attrs = { + noteable_type: 'MergeRequest', + noteable_id: @merge_request.id + } + + @diff_notes_disabled = false + + @use_legacy_diff_notes = !@merge_request.has_complete_diff_refs? + + @grouped_diff_discussions = @merge_request.grouped_diff_discussions(@compare.diff_refs) + @notes = prepare_notes_for_rendering(@grouped_diff_discussions.values.flatten.flat_map(&:notes)) + end +end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 879ff6d393e..04f8e95aa09 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -1,38 +1,17 @@ -class Projects::MergeRequestsController < Projects::ApplicationController +class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationController include ToggleSubscriptionAction - include DiffForPath - include DiffHelper include IssuableActions include RendersNotes include ToggleAwardEmoji include IssuableCollections - before_action :check_merge_requests_available! - before_action :merge_request, only: [ - :edit, :update, :show, :diffs, :commits, :conflicts, :conflict_for_path, :pipelines, :merge, - :pipeline_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_pipeline_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues, :commit_change_content - ] - before_action :validates_merge_request, only: [:show, :diffs, :commits, :pipelines] - before_action :define_show_vars, only: [:diffs, :commits, :conflicts, :conflict_for_path, :builds, :pipelines] - before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds, :conflicts, :conflict_for_path, :pipelines] - before_action :close_merge_request_without_source_project, only: [:show, :diffs, :commits, :builds, :pipelines] - before_action :check_if_can_be_merged, only: :show - before_action :apply_diff_view_cookie!, only: [:new_diffs] - before_action :build_merge_request, only: [:new, :new_diffs] - - # Allow read any merge_request - before_action :authorize_read_merge_request! - - # Allow write(create) merge_request - before_action :authorize_create_merge_request!, only: [:new, :create] - - # Allow modify merge_request + skip_before_action :merge_request, only: [:index, :bulk_update] + skip_before_action :ensure_ref_fetched, only: [:index, :bulk_update] + before_action :authorize_update_merge_request!, only: [:close, :edit, :update, :remove_wip, :sort] before_action :authenticate_user!, only: [:assign_related_issues] - before_action :authorize_can_resolve_conflicts!, only: [:conflicts, :conflict_for_path, :resolve_conflicts] - def index @collection_type = "MergeRequest" @merge_requests = merge_requests_collection @@ -72,10 +51,30 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def show + validates_merge_request + ensure_ref_fetched + close_merge_request_without_source_project + check_if_can_be_merged + respond_to do |format| format.html do - define_discussion_vars - define_show_vars + # Build a note object for comment form + @note = @project.notes.new(noteable: @merge_request) + + @discussions = @merge_request.discussions + @notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes)) + + @noteable = @merge_request + @commits_count = @merge_request.commits_count + + if @merge_request.locked_long_ago? + @merge_request.unlock_mr + @merge_request.close + end + + labels + + set_pipeline_variables end format.json do @@ -98,198 +97,40 @@ class Projects::MergeRequestsController < Projects::ApplicationController end end - def diffs - apply_diff_view_cookie! - - respond_to do |format| - format.html { define_discussion_vars } - format.json do - define_diff_vars - define_diff_comment_vars - - @environment = @merge_request.environments_for(current_user).last - - render json: { html: view_to_html_string("projects/merge_requests/show/_diffs") } - end - end - end - - # With an ID param, loads the MR at that ID. Otherwise, accepts the same params as #new - # and uses that (unsaved) MR. - # - def diff_for_path - if params[:id] - merge_request - define_diff_vars - define_diff_comment_vars - else - build_merge_request - @compare = @merge_request - @diffs = @compare.diffs(diff_options) - @diff_notes_disabled = true - end - - render_diff_for_path(@diffs) - end - def commits - respond_to do |format| - format.html do - define_discussion_vars - - render 'show' - end - format.json do - # Get commits from repository - # or from cache if already merged - @commits = @merge_request.commits - @note_counts = Note.where(commit_id: @commits.map(&:id)) - .group(:commit_id).count - - render json: { html: view_to_html_string('projects/merge_requests/show/_commits') } - end - end - end - - def conflicts - respond_to do |format| - format.html { define_discussion_vars } - - format.json do - if @conflicts_list.can_be_resolved_in_ui? - render json: @conflicts_list - elsif @merge_request.can_be_merged? - render json: { - message: 'The merge conflicts for this merge request have already been resolved. Please return to the merge request.', - type: 'error' - } - else - render json: { - message: 'The merge conflicts for this merge request cannot be resolved through GitLab. Please try to resolve them locally.', - type: 'error' - } - end - end - end - end - - def conflict_for_path - return render_404 unless @conflicts_list.can_be_resolved_in_ui? - - file = @conflicts_list.file_for_path(params[:old_path], params[:new_path]) - - return render_404 unless file - - render json: file, full_content: true - end - - def resolve_conflicts - return render_404 unless @conflicts_list.can_be_resolved_in_ui? - - if @merge_request.can_be_merged? - render status: :bad_request, json: { message: 'The merge conflicts for this merge request have already been resolved.' } - return - end - - begin - MergeRequests::Conflicts::ResolveService - .new(merge_request) - .execute(current_user, params) - - flash[:notice] = 'All merge conflicts were resolved. The merge request can now be merged.' + # Get commits from repository + # or from cache if already merged + @commits = @merge_request.commits + @note_counts = Note.where(commit_id: @commits.map(&:id)) + .group(:commit_id).count - render json: { redirect_to: namespace_project_merge_request_url(@project.namespace, @project, @merge_request, resolved_conflicts: true) } - rescue Gitlab::Conflict::ResolutionError => e - render status: :bad_request, json: { message: e.message } - end + render json: { html: view_to_html_string('projects/merge_requests/_commits') } end def pipelines @pipelines = @merge_request.all_pipelines - respond_to do |format| - format.html do - define_discussion_vars - - render 'show' - end - - format.json do - Gitlab::PollingInterval.set_header(response, interval: 10_000) + Gitlab::PollingInterval.set_header(response, interval: 10_000) - render json: PipelineSerializer - .new(project: @project, current_user: @current_user) - .represent(@pipelines) - end - end - end - - def new - respond_to do |format| - format.html { define_new_vars } - format.json do - define_pipelines_vars - - Gitlab::PollingInterval.set_header(response, interval: 10_000) - - render json: { - pipelines: PipelineSerializer - .new(project: @project, current_user: @current_user) - .represent(@pipelines) - } - end - end - end - - def new_diffs - respond_to do |format| - format.html do - define_new_vars - @show_changes_tab = true - render "new" - end - format.json do - @diffs = if @merge_request.can_be_created - @merge_request.diffs(diff_options) - else - [] - end - @diff_notes_disabled = true - - @environment = @merge_request.environments_for(current_user).last - - render json: { html: view_to_html_string('projects/merge_requests/_new_diffs', diffs: @diffs, environment: @environment) } - end - end - end - - def create - @target_branches ||= [] - @merge_request = MergeRequests::CreateService.new(project, current_user, merge_request_params).execute - - if @merge_request.valid? - redirect_to(merge_request_path(@merge_request)) - else - @source_project = @merge_request.source_project - @target_project = @merge_request.target_project - render action: "new" - end + render json: PipelineSerializer + .new(project: @project, current_user: @current_user) + .represent(@pipelines) end def edit - @source_project = @merge_request.source_project - @target_project = @merge_request.target_project - @target_branches = @merge_request.target_project.repository.branch_names + define_edit_vars end def update - @merge_request = MergeRequests::UpdateService.new(project, current_user, merge_request_params).execute(@merge_request) + @merge_request = ::MergeRequests::UpdateService.new(project, current_user, merge_request_params).execute(@merge_request) respond_to do |format| format.html do if @merge_request.valid? redirect_to([@merge_request.target_project.namespace.becomes(Namespace), @merge_request.target_project, @merge_request]) else + define_edit_vars + render :edit end end @@ -299,11 +140,13 @@ class Projects::MergeRequestsController < Projects::ApplicationController end end rescue ActiveRecord::StaleObjectError + define_edit_vars if request.format.html? + render_conflict_response end def remove_wip - @merge_request = MergeRequests::UpdateService + @merge_request = ::MergeRequests::UpdateService .new(project, current_user, wip_event: 'unwip') .execute(@merge_request) @@ -319,7 +162,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController return access_denied! end - MergeRequests::MergeWhenPipelineSucceedsService + ::MergeRequests::MergeWhenPipelineSucceedsService .new(@project, current_user) .cancel(@merge_request) @@ -338,53 +181,19 @@ class Projects::MergeRequestsController < Projects::ApplicationController end end - def branch_from - # This is always source - @source_project = @merge_request.nil? ? @project : @merge_request.source_project - - if params[:ref].present? - @ref = params[:ref] - @commit = @repository.commit("refs/heads/#{@ref}") - end - - render layout: false - end - - def branch_to - @target_project = selected_target_project - - if params[:ref].present? - @ref = params[:ref] - @commit = @target_project.commit("refs/heads/#{@ref}") - end - - render layout: false - end - - def update_branches - @target_project = selected_target_project - @target_branches = @target_project.repository.branch_names - - render layout: false - end - def assign_related_issues - result = MergeRequests::AssignIssuesService.new(project, current_user, merge_request: @merge_request).execute + result = ::MergeRequests::AssignIssuesService.new(project, current_user, merge_request: @merge_request).execute - respond_to do |format| - format.html do - case result[:count] - when 0 - flash[:error] = "Failed to assign you issues related to the merge request" - when 1 - flash[:notice] = "1 issue has been assigned to you" - else - flash[:notice] = "#{result[:count]} issues have been assigned to you" - end - - redirect_to(merge_request_path(@merge_request)) - end + case result[:count] + when 0 + flash[:error] = "Failed to assign you issues related to the merge request" + when 1 + flash[:notice] = "1 issue has been assigned to you" + else + flash[:notice] = "#{result[:count]} issues have been assigned to you" end + + redirect_to(merge_request_path(@merge_request)) end def pipeline_status @@ -432,17 +241,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController protected - def selected_target_project - if @project.id.to_s == params[:target_project_id] || @project.forked_project_link.nil? - @project - else - @project.forked_project_link.forked_from_project - end - end - - def merge_request - @issuable = @merge_request ||= @project.merge_requests.find_by!(iid: params[:id]) - end alias_method :subscribable_resource, :merge_request alias_method :issuable, :merge_request alias_method :awardable, :merge_request @@ -455,12 +253,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController return render_404 unless can?(current_user, :admin_merge_request, @merge_request) end - def authorize_can_resolve_conflicts! - @conflicts_list = MergeRequests::Conflicts::ListService.new(@merge_request) - - return render_404 unless @conflicts_list.can_be_resolved_by?(current_user) - end - def validates_merge_request # Show git not found page # if there is no saved commits between source & target branch @@ -470,133 +262,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController end end - def define_show_vars - @noteable = @merge_request - @commits_count = @merge_request.commits_count - - if @merge_request.locked_long_ago? - @merge_request.unlock_mr - @merge_request.close - end - - labels - define_pipelines_vars - end - - # Discussion tab data is rendered on html responses of actions - # :show, :diff, :commits, :builds. but not when request the data through AJAX - def define_discussion_vars - # Build a note object for comment form - @note = @project.notes.new(noteable: @merge_request) - - @discussions = @merge_request.discussions - @notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes)) - end - - def define_diff_vars - @merge_request_diff = - if params[:diff_id] - @merge_request.merge_request_diffs.viewable.find(params[:diff_id]) - else - @merge_request.merge_request_diff - end - - @merge_request_diffs = @merge_request.merge_request_diffs.viewable.select_without_diff - @comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id } - - if params[:start_sha].present? - @start_sha = params[:start_sha] - @start_version = @comparable_diffs.find { |diff| diff.head_commit_sha == @start_sha } - - unless @start_version - @start_sha = @merge_request_diff.head_commit_sha - @start_version = @merge_request_diff - end - end - - @compare = - if @start_sha - @merge_request_diff.compare_with(@start_sha) - else - @merge_request_diff - end - - @diffs = @compare.diffs(diff_options) - end - - def define_diff_comment_vars - @new_diff_note_attrs = { - noteable_type: 'MergeRequest', - noteable_id: @merge_request.id - } - - @diff_notes_disabled = false - - @use_legacy_diff_notes = !@merge_request.has_complete_diff_refs? - - @grouped_diff_discussions = @merge_request.grouped_diff_discussions(@compare.diff_refs) - @notes = prepare_notes_for_rendering(@grouped_diff_discussions.values.flatten.flat_map(&:notes)) - end - - def define_pipelines_vars - @pipelines = @merge_request.all_pipelines - @pipeline = @merge_request.head_pipeline - @statuses_count = @pipeline.present? ? @pipeline.statuses.relevant.count : 0 - end - - def define_new_vars - @noteable = @merge_request - - @target_branches = if @merge_request.target_project - @merge_request.target_project.repository.branch_names - else - [] - end - - @target_project = merge_request.target_project - @source_project = merge_request.source_project - @commits = @merge_request.compare_commits.reverse - @commit = @merge_request.diff_head_commit - - @note_counts = Note.where(commit_id: @commits.map(&:id)) - .group(:commit_id).count - - @labels = LabelsFinder.new(current_user, project_id: @project.id).execute - - @show_changes_tab = params[:show_changes].present? - - define_pipelines_vars - end - def invalid_mr # Render special view for MR with removed target branch render 'invalid' end - def merge_request_params - params.require(:merge_request) - .permit(merge_request_params_attributes) - end - - def merge_request_params_attributes - [ - :assignee_id, - :description, - :force_remove_source_branch, - :lock_version, - :milestone_id, - :source_branch, - :source_project_id, - :state_event, - :target_branch, - :target_project_id, - :task_num, - :title, - - label_ids: [] - ] - end - def merge_params params.permit(merge_params_attributes) end @@ -605,22 +275,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController [:should_remove_source_branch, :commit_message] end - # Make sure merge requests created before 8.0 - # have head file in refs/merge-requests/ - def ensure_ref_fetched - @merge_request.ensure_ref_fetched - end - def merge_when_pipeline_succeeds_active? params[:merge_when_pipeline_succeeds].present? && @merge_request.head_pipeline && @merge_request.head_pipeline.active? end - def build_merge_request - params[:merge_request] ||= ActionController::Parameters.new(source_project: @project) - @merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params.merge(diff_options: diff_options)).execute - end - def close_merge_request_without_source_project if !@merge_request.source_project && @merge_request.open? @merge_request.close @@ -648,7 +307,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController return :failed unless @merge_request.head_pipeline if @merge_request.head_pipeline.active? - MergeRequests::MergeWhenPipelineSucceedsService + ::MergeRequests::MergeWhenPipelineSucceedsService .new(@project, current_user, merge_params) .execute(@merge_request) @@ -672,4 +331,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController def serializer MergeRequestSerializer.new(current_user: current_user, project: merge_request.project) end + + def define_edit_vars + @source_project = @merge_request.source_project + @target_project = @merge_request.target_project + @target_branches = @merge_request.target_project.repository.branch_names + end end diff --git a/app/controllers/projects/pipeline_schedules_controller.rb b/app/controllers/projects/pipeline_schedules_controller.rb index ef4f083b98f..60db179277b 100644 --- a/app/controllers/projects/pipeline_schedules_controller.rb +++ b/app/controllers/projects/pipeline_schedules_controller.rb @@ -1,6 +1,7 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController before_action :authorize_read_pipeline_schedule! - before_action :authorize_create_pipeline_schedule!, only: [:new, :create, :edit, :take_ownership, :update] + before_action :authorize_create_pipeline_schedule!, only: [:new, :create] + before_action :authorize_update_pipeline_schedule!, only: [:edit, :take_ownership, :update] before_action :authorize_admin_pipeline_schedule!, only: [:destroy] before_action :schedule, only: [:edit, :update, :destroy, :take_ownership] diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 5480814874b..450895cdf3a 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -97,7 +97,7 @@ class ProjectsController < Projects::ApplicationController end if @project.pending_delete? - flash[:alert] = _("Project '%{project_name}' queued for deletion.") % { project_name: @project.name } + flash.now[:alert] = _("Project '%{project_name}' queued for deletion.") % { project_name: @project.name } end respond_to do |format| diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb index c358f23f541..3fe37c75381 100644 --- a/app/finders/todos_finder.rb +++ b/app/finders/todos_finder.rb @@ -83,6 +83,8 @@ class TodosFinder if project? @project = Project.find(params[:project_id]) + @project = nil if @project.pending_delete? + unless Ability.allowed?(current_user, :read_project, @project) @project = nil end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index dc7ff78f3df..7be8e3b96cf 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -131,10 +131,7 @@ module ApplicationHelper end def body_data_page - path = controller.controller_path.split('/') - namespace = path.first if path.second - - [namespace, controller.controller_name, controller.action_name].compact.join(':') + [*controller.controller_path.split('/'), controller.action_name].compact.join(':') end # shortcut for gitlab config diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 3efa7c36057..ee36617ba9a 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -284,7 +284,7 @@ module BlobHelper merge_project = can?(current_user, :create_merge_request, project) ? project : (current_user && current_user.fork_of(project)) if merge_project - options << link_to("create a merge request", new_namespace_project_merge_request_path(project.namespace, project)) + options << link_to("create a merge request", namespace_project_new_merge_request_path(project.namespace, project)) end options diff --git a/app/helpers/compare_helper.rb b/app/helpers/compare_helper.rb index 2aa0449c46e..424ded2b69d 100644 --- a/app/helpers/compare_helper.rb +++ b/app/helpers/compare_helper.rb @@ -9,7 +9,7 @@ module CompareHelper end def create_mr_path(from = params[:from], to = params[:to], project = @project) - new_namespace_project_merge_request_path( + namespace_project_new_merge_request_path( project.namespace, project, merge_request: { diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index 39d30631646..54d6f86fa11 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -1,7 +1,7 @@ module MergeRequestsHelper def new_mr_path_from_push_event(event) target_project = event.project.default_merge_request_target - new_namespace_project_merge_request_path( + namespace_project_new_merge_request_path( event.project.namespace, event.project, new_mr_from_push_event(event, target_project) @@ -48,7 +48,7 @@ module MergeRequestsHelper end def mr_change_branches_path(merge_request) - new_namespace_project_merge_request_path( + namespace_project_new_merge_request_path( @project.namespace, @project, merge_request: { source_project_id: merge_request.source_project_id, diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index 833d3c36b28..e589ed4e56d 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -1,11 +1,7 @@ module NavHelper def page_gutter_class if current_path?('merge_requests#show') || - current_path?('merge_requests#diffs') || - current_path?('merge_requests#commits') || - current_path?('merge_requests#builds') || - current_path?('merge_requests#conflicts') || - current_path?('merge_requests#pipelines') || + current_path?('projects/merge_requests/conflicts#show') || current_path?('issues#show') || current_path?('milestones#show') if cookies[:collapsed_gutter] == 'true' diff --git a/app/helpers/submodule_helper.rb b/app/helpers/submodule_helper.rb index 8e0a1e2ecdf..b24039fb349 100644 --- a/app/helpers/submodule_helper.rb +++ b/app/helpers/submodule_helper.rb @@ -73,6 +73,7 @@ module SubmoduleHelper end def relative_self_links(url, commit) + url.rstrip! # Map relative links to a namespace and project # For example: # ../bar.git -> same namespace, repo bar diff --git a/app/models/ability.rb b/app/models/ability.rb index f3692a5a067..d2b8a8447b5 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -1,35 +1,20 @@ +require 'declarative_policy' + class Ability class << self # Given a list of users and a project this method returns the users that can # read the given project. def users_that_can_read_project(users, project) - if project.public? - users - else - users.select do |user| - if user.admin? - true - elsif project.internal? && !user.external? - true - elsif project.owner == user - true - elsif project.team.members.include?(user) - true - else - false - end - end + DeclarativePolicy.subject_scope do + users.select { |u| allowed?(u, :read_project, project) } end end # Given a list of users and a snippet this method returns the users that can # read the given snippet. def users_that_can_read_personal_snippet(users, snippet) - case snippet.visibility_level - when Snippet::INTERNAL, Snippet::PUBLIC - users - when Snippet::PRIVATE - users.include?(snippet.author) ? [snippet.author] : [] + DeclarativePolicy.subject_scope do + users.select { |u| allowed?(u, :read_personal_snippet, snippet) } end end @@ -38,42 +23,35 @@ class Ability # issues - The issues to reduce down to those readable by the user. # user - The User for which to check the issues def issues_readable_by_user(issues, user = nil) - return issues if user && user.admin? - - issues.select { |issue| issue.visible_to_user?(user) } + DeclarativePolicy.user_scope do + issues.select { |issue| issue.visible_to_user?(user) } + end end - # TODO: make this private and use the actual abilities stuff for this def can_edit_note?(user, note) - return false if !note.editable? || !user.present? - return true if note.author == user || user.admin? - - if note.project - max_access_level = note.project.team.max_member_access(user.id) - max_access_level >= Gitlab::Access::MASTER - else - false - end + allowed?(user, :edit_note, note) end - def allowed?(user, action, subject = :global) - allowed(user, subject).include?(action) - end + def allowed?(user, action, subject = :global, opts = {}) + if subject.is_a?(Hash) + opts, subject = subject, :global + end - def allowed(user, subject = :global) - return BasePolicy::RuleSet.none if subject.nil? - return uncached_allowed(user, subject) unless RequestStore.active? + policy = policy_for(user, subject) - user_key = user ? user.id : 'anonymous' - subject_key = subject == :global ? 'global' : "#{subject.class.name}/#{subject.id}" - key = "/ability/#{user_key}/#{subject_key}" - RequestStore[key] ||= uncached_allowed(user, subject).freeze + case opts[:scope] + when :user + DeclarativePolicy.user_scope { policy.can?(action) } + when :subject + DeclarativePolicy.subject_scope { policy.can?(action) } + else + policy.can?(action) + end end - private - - def uncached_allowed(user, subject) - BasePolicy.class_for(subject).abilities(user, subject) + def policy_for(user, subject = :global) + cache = RequestStore.active? ? RequestStore : {} + DeclarativePolicy.policy_for(user, subject, cache: cache) end end end diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb index f235260208f..96d6e120998 100644 --- a/app/models/ci/variable.rb +++ b/app/models/ci/variable.rb @@ -1,27 +1,12 @@ module Ci class Variable < ActiveRecord::Base extend Ci::Model + include HasVariable belongs_to :project - validates :key, - presence: true, - uniqueness: { scope: :project_id }, - length: { maximum: 255 }, - format: { with: /\A[a-zA-Z0-9_]+\z/, - message: "can contain only letters, digits and '_'." } + validates :key, uniqueness: { scope: :project_id } - scope :order_key_asc, -> { reorder(key: :asc) } scope :unprotected, -> { where(protected: false) } - - attr_encrypted :value, - mode: :per_attribute_iv_and_salt, - insecure_mode: true, - key: Gitlab::Application.secrets.db_key_base, - algorithm: 'aes-256-cbc' - - def to_runner_variable - { key: key, value: value, public: false } - end end end diff --git a/app/models/concerns/has_variable.rb b/app/models/concerns/has_variable.rb new file mode 100644 index 00000000000..9585b5583dc --- /dev/null +++ b/app/models/concerns/has_variable.rb @@ -0,0 +1,23 @@ +module HasVariable + extend ActiveSupport::Concern + + included do + validates :key, + presence: true, + length: { maximum: 255 }, + format: { with: /\A[a-zA-Z0-9_]+\z/, + message: "can contain only letters, digits and '_'." } + + scope :order_key_asc, -> { reorder(key: :asc) } + + attr_encrypted :value, + mode: :per_attribute_iv_and_salt, + insecure_mode: true, + key: Gitlab::Application.secrets.db_key_base, + algorithm: 'aes-256-cbc' + + def to_runner_variable + { key: key, value: value, public: false } + end + end +end diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb index a155a064032..fdacfa5a194 100644 --- a/app/models/concerns/sortable.rb +++ b/app/models/concerns/sortable.rb @@ -5,6 +5,25 @@ module Sortable extend ActiveSupport::Concern + module DropDefaultScopeOnFinders + # Override these methods to drop the `ORDER BY id DESC` default scope. + # See http://dba.stackexchange.com/a/110919 for why we do this. + %i[find find_by find_by!].each do |meth| + define_method meth do |*args, &block| + return super(*args, &block) if block + + unordered_relation = unscope(:order) + + # We cannot simply call `meth` on `unscope(:order)`, since that is also + # an instance of the same relation class this module is included into, + # which means we'd get infinite recursion. + # We explicitly use the original implementation to prevent this. + original_impl = method(__method__).super_method.unbind + original_impl.bind(unordered_relation).call(*args) + end + end + end + included do # By default all models should be ordered # by created_at field starting from newest @@ -18,6 +37,10 @@ module Sortable scope :order_updated_asc, -> { reorder(updated_at: :asc) } scope :order_name_asc, -> { reorder(name: :asc) } scope :order_name_desc, -> { reorder(name: :desc) } + + # All queries (relations) on this model are instances of this `relation_klass`. + relation_klass = relation_delegate_class(ActiveRecord::Relation) + relation_klass.prepend DropDefaultScopeOnFinders end module ClassMethods diff --git a/app/models/group.rb b/app/models/group.rb index 0b93460d473..a6fdb30f84c 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -222,6 +222,12 @@ class Group < Namespace User.where(id: members_with_parents.select(:user_id)) end + def users_with_descendants + members_with_descendants = GroupMember.non_request.where(source_id: descendants.pluck(:id).push(id)) + + User.where(id: members_with_descendants.select(:user_id)) + end + def max_member_access_for_user(user) return GroupMember::OWNER if user.admin? diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 583d4fb5244..efbed5a2ef5 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -47,6 +47,8 @@ class Namespace < ActiveRecord::Base before_destroy(prepend: true) { prepare_for_destroy } after_destroy :rm_dir + default_scope { with_deleted } + scope :for_user, -> { where('type IS NULL') } scope :with_statistics, -> do diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb index b0df7aeb323..81844b1e2ca 100644 --- a/app/models/notification_setting.rb +++ b/app/models/notification_setting.rb @@ -19,7 +19,7 @@ class NotificationSetting < ActiveRecord::Base # pending delete). # scope :for_projects, -> do - includes(:project).references(:projects).where(source_type: 'Project').where.not(projects: { id: nil }) + includes(:project).references(:projects).where(source_type: 'Project').where.not(projects: { id: nil, pending_delete: true }) end EMAIL_EVENTS = [ diff --git a/app/models/project.rb b/app/models/project.rb index 35d8f9a0154..228f66b95cd 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -222,9 +222,8 @@ class Project < ActiveRecord::Base has_many :uploads, as: :model, dependent: :destroy # Scopes - default_scope { where(pending_delete: false) } - - scope :with_deleted, -> { unscope(where: :pending_delete) } + scope :pending_delete, -> { where(pending_delete: true) } + scope :without_deleted, -> { where(pending_delete: false) } scope :sorted_by_activity, -> { reorder(last_activity_at: :desc) } scope :sorted_by_stars, -> { reorder('projects.star_count DESC') } @@ -352,7 +351,16 @@ class Project < ActiveRecord::Base after_transition started: :finished do |project, _| project.reset_cache_and_import_attrs - project.perform_housekeeping + + if Gitlab::ImportSources.importer_names.include?(project.import_type) && project.repo_exists? + project.run_after_commit do + begin + Projects::HousekeepingService.new(project).execute + rescue Projects::HousekeepingService::LeaseTaken => e + Rails.logger.info("Could not perform housekeeping for project #{project.path_with_namespace} (#{project.id}): #{e}") + end + end + end end end @@ -510,22 +518,6 @@ class Project < ActiveRecord::Base ProjectCacheWorker.perform_async(self.id) end - remove_import_data - end - - def perform_housekeeping - return unless repo_exists? - - run_after_commit do - begin - Projects::HousekeepingService.new(self).execute - rescue Projects::HousekeepingService::LeaseTaken => e - Rails.logger.info("Could not perform housekeeping for project #{self.path_with_namespace} (#{self.id}): #{e}") - end - end - end - - def remove_import_data import_data&.destroy end @@ -823,7 +815,7 @@ class Project < ActiveRecord::Base end def ci_service - @ci_service ||= ci_services.reorder(nil).find_by(active: true) + @ci_service ||= ci_services.find_by(active: true) end def deployment_services @@ -831,7 +823,7 @@ class Project < ActiveRecord::Base end def deployment_service - @deployment_service ||= deployment_services.reorder(nil).find_by(active: true) + @deployment_service ||= deployment_services.find_by(active: true) end def monitoring_services @@ -839,7 +831,7 @@ class Project < ActiveRecord::Base end def monitoring_service - @monitoring_service ||= monitoring_services.reorder(nil).find_by(active: true) + @monitoring_service ||= monitoring_services.find_by(active: true) end def jira_tracker? @@ -1460,7 +1452,7 @@ class Project < ActiveRecord::Base def pending_delete_twin return false unless path - Project.unscoped.where(pending_delete: true).find_by_full_path(path_with_namespace) + Project.pending_delete.find_by_full_path(path_with_namespace) end ## diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb index 48edd0738ee..c8fabb16dc1 100644 --- a/app/models/project_feature.rb +++ b/app/models/project_feature.rb @@ -51,8 +51,11 @@ class ProjectFeature < ActiveRecord::Base default_value_for :repository_access_level, value: ENABLED, allows_nil: false def feature_available?(feature, user) - access_level = public_send(ProjectFeature.access_level_attribute(feature)) - get_permission(user, access_level) + get_permission(user, access_level(feature)) + end + + def access_level(feature) + public_send(ProjectFeature.access_level_attribute(feature)) end def builds_enabled? diff --git a/app/policies/base_policy.rb b/app/policies/base_policy.rb index 623424c63e0..00067ce756e 100644 --- a/app/policies/base_policy.rb +++ b/app/policies/base_policy.rb @@ -1,127 +1,13 @@ -class BasePolicy - class RuleSet - attr_reader :can_set, :cannot_set - def initialize(can_set, cannot_set) - @can_set = can_set - @cannot_set = cannot_set - end +require 'declarative_policy' - delegate :size, to: :to_set +class BasePolicy < DeclarativePolicy::Base + desc "User is an instance admin" + with_options scope: :user, score: 0 + condition(:admin) { @user&.admin? } - def self.empty - new(Set.new, Set.new) - end + with_options scope: :user, score: 0 + condition(:external_user) { @user.nil? || @user.external? } - def self.none - empty.freeze - end - - def can?(ability) - @can_set.include?(ability) && !@cannot_set.include?(ability) - end - - def include?(ability) - can?(ability) - end - - def to_set - @can_set - @cannot_set - end - - def merge(other) - @can_set.merge(other.can_set) - @cannot_set.merge(other.cannot_set) - end - - def can!(*abilities) - @can_set.merge(abilities) - end - - def cannot!(*abilities) - @cannot_set.merge(abilities) - end - - def freeze - @can_set.freeze - @cannot_set.freeze - super - end - end - - def self.abilities(user, subject) - new(user, subject).abilities - end - - def self.class_for(subject) - return GlobalPolicy if subject == :global - raise ArgumentError, 'no policy for nil' if subject.nil? - - if subject.class.try(:presenter?) - subject = subject.subject - end - - subject.class.ancestors.each do |klass| - next unless klass.name - - begin - policy_class = "#{klass.name}Policy".constantize - - # NOTE: the < operator here tests whether policy_class - # inherits from BasePolicy - return policy_class if policy_class < BasePolicy - rescue NameError - nil - end - end - - raise "no policy for #{subject.class.name}" - end - - attr_reader :user, :subject - def initialize(user, subject) - @user = user - @subject = subject - end - - def abilities - return RuleSet.none if @user && @user.blocked? - return anonymous_abilities if @user.nil? - collect_rules { rules } - end - - def anonymous_abilities - collect_rules { anonymous_rules } - end - - def anonymous_rules - rules - end - - def rules - raise NotImplementedError - end - - def delegate!(new_subject) - @rule_set.merge(Ability.allowed(@user, new_subject)) - end - - def can?(rule) - @rule_set.can?(rule) - end - - def can!(*rules) - @rule_set.can!(*rules) - end - - def cannot!(*rules) - @rule_set.cannot!(*rules) - end - - private - - def collect_rules(&b) - @rule_set = RuleSet.empty - yield - @rule_set - end + with_options scope: :user, score: 0 + condition(:can_create_group) { @user&.can_create_group } end diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb index 2d7405dc240..a886efc1360 100644 --- a/app/policies/ci/build_policy.rb +++ b/app/policies/ci/build_policy.rb @@ -1,29 +1,13 @@ module Ci class BuildPolicy < CommitStatusPolicy - alias_method :build, :subject - - def rules - super - - # If we can't read build we should also not have that - # ability when looking at this in context of commit_status - %w[read create update admin].each do |rule| - cannot! :"#{rule}_commit_status" unless can? :"#{rule}_build" - end - - if can?(:update_build) && protected_action? - cannot! :update_build - end - end - - private - - def protected_action? - return false unless build.action? + condition(:protected_action) do + next false unless @subject.action? !::Gitlab::UserAccess - .new(user, project: build.project) - .can_merge_to_branch?(build.ref) + .new(@user, project: @subject.project) + .can_merge_to_branch?(@subject.ref) end + + rule { protected_action }.prevent :update_build end end diff --git a/app/policies/ci/pipeline_policy.rb b/app/policies/ci/pipeline_policy.rb index 10aa2d3e72a..a2dde95dbc8 100644 --- a/app/policies/ci/pipeline_policy.rb +++ b/app/policies/ci/pipeline_policy.rb @@ -1,7 +1,5 @@ module Ci class PipelinePolicy < BasePolicy - def rules - delegate! @subject.project - end + delegate { @subject.project } end end diff --git a/app/policies/ci/runner_policy.rb b/app/policies/ci/runner_policy.rb index 416d93ffe63..7dff8470e23 100644 --- a/app/policies/ci/runner_policy.rb +++ b/app/policies/ci/runner_policy.rb @@ -1,13 +1,16 @@ module Ci class RunnerPolicy < BasePolicy - def rules - return unless @user + with_options scope: :subject, score: 0 + condition(:shared) { @subject.is_shared? } - can! :assign_runner if @user.admin? + with_options scope: :subject, score: 0 + condition(:locked, scope: :subject) { @subject.locked? } - return if @subject.is_shared? || @subject.locked? + condition(:authorized_runner) { @user.ci_authorized_runners.include?(@subject) } - can! :assign_runner if @user.ci_authorized_runners.include?(@subject) - end + rule { anonymous }.prevent_all + rule { admin | authorized_runner }.enable :assign_runner + rule { ~admin & shared }.prevent :assign_runner + rule { ~admin & locked }.prevent :assign_runner end end diff --git a/app/policies/ci/trigger_policy.rb b/app/policies/ci/trigger_policy.rb index c90c9ac0583..5592ac30812 100644 --- a/app/policies/ci/trigger_policy.rb +++ b/app/policies/ci/trigger_policy.rb @@ -1,13 +1,16 @@ module Ci class TriggerPolicy < BasePolicy - def rules - delegate! @subject.project - - if can?(:admin_build) - can! :admin_trigger if @subject.owner.blank? || - @subject.owner == @user - can! :manage_trigger - end - end + delegate { @subject.project } + + with_options scope: :subject, score: 0 + condition(:legacy) { @subject.legacy? } + + with_score 0 + condition(:is_owner) { @user && @subject.owner_id == @user.id } + + rule { ~can?(:admin_build) }.prevent :admin_trigger + rule { legacy | is_owner }.enable :admin_trigger + + rule { can?(:admin_build) }.enable :manage_trigger end end diff --git a/app/policies/commit_status_policy.rb b/app/policies/commit_status_policy.rb index 593df738328..24b2a4cc7fd 100644 --- a/app/policies/commit_status_policy.rb +++ b/app/policies/commit_status_policy.rb @@ -1,5 +1,7 @@ class CommitStatusPolicy < BasePolicy - def rules - delegate! @subject.project + delegate { @subject.project } + + %w[read create update admin].each do |action| + rule { ~can?(:"#{action}_commit_status") }.prevent :"#{action}_build" end end diff --git a/app/policies/deploy_key_policy.rb b/app/policies/deploy_key_policy.rb index ebab213e6be..62a22a59be6 100644 --- a/app/policies/deploy_key_policy.rb +++ b/app/policies/deploy_key_policy.rb @@ -1,11 +1,11 @@ class DeployKeyPolicy < BasePolicy - def rules - return unless @user + with_options scope: :subject, score: 0 + condition(:private_deploy_key) { @subject.private? } - can! :update_deploy_key if @user.admin? + condition(:has_deploy_key) { @user.project_deploy_keys.exists?(id: @subject.id) } - if @subject.private? && @user.project_deploy_keys.exists?(id: @subject.id) - can! :update_deploy_key - end - end + rule { anonymous }.prevent_all + + rule { admin }.enable :update_deploy_key + rule { private_deploy_key & has_deploy_key }.enable :update_deploy_key end diff --git a/app/policies/deployment_policy.rb b/app/policies/deployment_policy.rb index 163d070ff90..62b63b9f87b 100644 --- a/app/policies/deployment_policy.rb +++ b/app/policies/deployment_policy.rb @@ -1,5 +1,3 @@ class DeploymentPolicy < BasePolicy - def rules - delegate! @subject.project - end + delegate { @subject.project } end diff --git a/app/policies/environment_policy.rb b/app/policies/environment_policy.rb index 2fa15e64562..375a5535359 100644 --- a/app/policies/environment_policy.rb +++ b/app/policies/environment_policy.rb @@ -1,17 +1,9 @@ class EnvironmentPolicy < BasePolicy - alias_method :environment, :subject + delegate { @subject.project } - def rules - delegate! environment.project - - if can?(:create_deployment) && environment.stop_action? - can! :stop_environment if can_play_stop_action? - end + condition(:stop_action_allowed) do + @subject.stop_action? && can?(:update_build, @subject.stop_action) end - private - - def can_play_stop_action? - Ability.allowed?(user, :update_build, environment.stop_action) - end + rule { can?(:create_deployment) & stop_action_allowed }.enable :stop_environment end diff --git a/app/policies/external_issue_policy.rb b/app/policies/external_issue_policy.rb index d9e28bd107a..e031b38078c 100644 --- a/app/policies/external_issue_policy.rb +++ b/app/policies/external_issue_policy.rb @@ -1,5 +1,3 @@ class ExternalIssuePolicy < BasePolicy - def rules - delegate! @subject.project - end + delegate { @subject.project } end diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb index 2683aaad981..535faa922dd 100644 --- a/app/policies/global_policy.rb +++ b/app/policies/global_policy.rb @@ -1,16 +1,40 @@ class GlobalPolicy < BasePolicy - def rules - return unless @user + desc "User is blocked" + with_options scope: :user, score: 0 + condition(:blocked) { @user.blocked? } - can! :create_group if @user.can_create_group - can! :read_users_list + desc "User is an internal user" + with_options scope: :user, score: 0 + condition(:internal) { @user.internal? } - unless @user.blocked? || @user.internal? - can! :log_in unless @user.access_locked? - can! :access_api - can! :access_git - can! :receive_notifications - can! :use_quick_actions - end + desc "User's access has been locked" + with_options scope: :user, score: 0 + condition(:access_locked) { @user.access_locked? } + + rule { anonymous }.prevent_all + + rule { default }.policy do + enable :read_users_list + enable :log_in + enable :access_api + enable :access_git + enable :receive_notifications + enable :use_quick_actions + end + + rule { blocked | internal }.policy do + prevent :log_in + prevent :access_api + prevent :access_git + prevent :receive_notifications + prevent :use_quick_actions + end + + rule { can_create_group }.policy do + enable :create_group + end + + rule { access_locked }.policy do + prevent :log_in end end diff --git a/app/policies/group_label_policy.rb b/app/policies/group_label_policy.rb index 7b34aa182eb..e3dd3296699 100644 --- a/app/policies/group_label_policy.rb +++ b/app/policies/group_label_policy.rb @@ -1,5 +1,3 @@ class GroupLabelPolicy < BasePolicy - def rules - delegate! @subject.group - end + delegate { @subject.group } end diff --git a/app/policies/group_member_policy.rb b/app/policies/group_member_policy.rb index 5a3fe814b77..23dd0d7cd23 100644 --- a/app/policies/group_member_policy.rb +++ b/app/policies/group_member_policy.rb @@ -1,25 +1,22 @@ class GroupMemberPolicy < BasePolicy - def rules - return unless @user + delegate :group - target_user = @subject.user - group = @subject.group + with_scope :subject + condition(:last_owner) { @subject.group.last_owner?(@subject.user) } - return if group.last_owner?(target_user) + desc "Membership is users' own" + with_score 0 + condition(:is_target_user) { @user && @subject.user_id == @user.id } - can_manage = Ability.allowed?(@user, :admin_group_member, group) + rule { anonymous }.prevent_all + rule { last_owner }.prevent_all - if can_manage - can! :update_group_member - can! :destroy_group_member - elsif @user == target_user - can! :destroy_group_member - end - - additional_rules! + rule { can?(:admin_group_member) }.policy do + enable :update_group_member + enable :destroy_group_member end - def additional_rules! - # This is meant to be overriden in EE + rule { is_target_user }.policy do + enable :destroy_group_member end end diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index fb07298c6c2..dcb37416ca3 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -1,50 +1,58 @@ class GroupPolicy < BasePolicy - def rules - can! :read_group if @subject.public? - return unless @user - - globally_viewable = @subject.public? || (@subject.internal? && !@user.external?) - access_level = @subject.max_member_access_for_user(@user) - owner = access_level >= GroupMember::OWNER - master = access_level >= GroupMember::MASTER - reporter = access_level >= GroupMember::REPORTER - - can_read = false - can_read ||= globally_viewable - can_read ||= access_level >= GroupMember::GUEST - can_read ||= GroupProjectsFinder.new(group: @subject, current_user: @user).execute.any? - can! :read_group if can_read - - if reporter - can! :admin_label - end - - # Only group masters and group owners can create new projects - if master - can! :create_projects - can! :admin_milestones - end - - # Only group owner and administrators can admin group - if owner - can! :admin_group - can! :admin_namespace - can! :admin_group_member - can! :change_visibility_level - can! :create_subgroup if @user.can_create_group - end - - if globally_viewable && @subject.request_access_enabled && access_level == GroupMember::NO_ACCESS - can! :request_access - end - end + desc "Group is public" + with_options scope: :subject, score: 0 + condition(:public_group) { @subject.public? } + + with_score 0 + condition(:logged_in_viewable) { @user && @subject.internal? && !@user.external? } + + condition(:has_access) { access_level != GroupMember::NO_ACCESS } - def can_read_group? - return true if @subject.public? - return true if @user.admin? - return true if @subject.internal? && !@user.external? - return true if @subject.users.include?(@user) + condition(:guest) { access_level >= GroupMember::GUEST } + condition(:owner) { access_level >= GroupMember::OWNER } + condition(:master) { access_level >= GroupMember::MASTER } + condition(:reporter) { access_level >= GroupMember::REPORTER } + condition(:has_projects) do GroupProjectsFinder.new(group: @subject, current_user: @user).execute.any? end + + with_options scope: :subject, score: 0 + condition(:request_access_enabled) { @subject.request_access_enabled } + + rule { public_group } .enable :read_group + rule { logged_in_viewable }.enable :read_group + rule { guest } .enable :read_group + rule { admin } .enable :read_group + rule { has_projects } .enable :read_group + + rule { reporter }.enable :admin_label + + rule { master }.policy do + enable :create_projects + enable :admin_milestones + end + + rule { owner }.policy do + enable :admin_group + enable :admin_namespace + enable :admin_group_member + enable :change_visibility_level + end + + rule { owner & can_create_group }.enable :create_subgroup + + rule { public_group | logged_in_viewable }.enable :view_globally + + rule { default }.enable(:request_access) + + rule { ~request_access_enabled }.prevent :request_access + rule { ~can?(:view_globally) }.prevent :request_access + rule { has_access }.prevent :request_access + + def access_level + return GroupMember::NO_ACCESS if @user.nil? + + @access_level ||= @subject.max_member_access_for_user(@user) + end end diff --git a/app/policies/issuable_policy.rb b/app/policies/issuable_policy.rb index 9501e499507..daf6fa9e18a 100644 --- a/app/policies/issuable_policy.rb +++ b/app/policies/issuable_policy.rb @@ -1,14 +1,15 @@ class IssuablePolicy < BasePolicy - def action_name - @subject.class.name.underscore - end + delegate { @subject.project } - def rules - if @user && @subject.assignee_or_author?(@user) - can! :"read_#{action_name}" - can! :"update_#{action_name}" - end + desc "User is the assignee or author" + condition(:assignee_or_author) do + @user && @subject.assignee_or_author?(@user) + end - delegate! @subject.project + rule { assignee_or_author }.policy do + enable :read_issue + enable :update_issue + enable :read_merge_request + enable :update_merge_request end end diff --git a/app/policies/issue_policy.rb b/app/policies/issue_policy.rb index 88f3179c6ff..bd2d417b2a8 100644 --- a/app/policies/issue_policy.rb +++ b/app/policies/issue_policy.rb @@ -3,25 +3,17 @@ class IssuePolicy < IssuablePolicy # Make sure to sync this class checks with issue.rb to avoid security problems. # Check commit 002ad215818450d2cbbc5fa065850a953dc7ada8 for more information. - def issue - @subject + desc "User can read confidential issues" + condition(:can_read_confidential) do + @user && IssueCollection.new([@subject]).visible_to(@user).any? end - def rules - super + desc "Issue is confidential" + condition(:confidential, scope: :subject) { @subject.confidential? } - if @subject.confidential? && !can_read_confidential? - cannot! :read_issue - cannot! :update_issue - cannot! :admin_issue - end - end - - private - - def can_read_confidential? - return false unless @user - - IssueCollection.new([@subject]).visible_to(@user).any? + rule { confidential & ~can_read_confidential }.policy do + prevent :read_issue + prevent :update_issue + prevent :admin_issue end end diff --git a/app/policies/namespace_policy.rb b/app/policies/namespace_policy.rb index 29bb357e00a..85b67f0a237 100644 --- a/app/policies/namespace_policy.rb +++ b/app/policies/namespace_policy.rb @@ -1,10 +1,10 @@ class NamespacePolicy < BasePolicy - def rules - return unless @user + rule { anonymous }.prevent_all - if @subject.owner == @user || @user.admin? - can! :create_projects - can! :admin_namespace - end + condition(:owner) { @subject.owner == @user } + + rule { owner | admin }.policy do + enable :create_projects + enable :admin_namespace end end diff --git a/app/policies/nil_policy.rb b/app/policies/nil_policy.rb new file mode 100644 index 00000000000..13f46ba60f0 --- /dev/null +++ b/app/policies/nil_policy.rb @@ -0,0 +1,3 @@ +class NilPolicy < BasePolicy + rule { default }.prevent_all +end diff --git a/app/policies/note_policy.rb b/app/policies/note_policy.rb index 5326061bd07..20cd51cfb99 100644 --- a/app/policies/note_policy.rb +++ b/app/policies/note_policy.rb @@ -1,19 +1,24 @@ class NotePolicy < BasePolicy - def rules - delegate! @subject.project + delegate { @subject.project } - return unless @user + condition(:is_author) { @user && @subject.author == @user } + condition(:for_merge_request, scope: :subject) { @subject.for_merge_request? } + condition(:is_noteable_author) { @user && @subject.noteable.author_id == @user.id } - if @subject.author == @user - can! :read_note - can! :update_note - can! :admin_note - can! :resolve_note - end + condition(:editable, scope: :subject) { @subject.editable? } - if @subject.for_merge_request? && - @subject.noteable.author == @user - can! :resolve_note - end + rule { ~editable | anonymous }.prevent :edit_note + rule { is_author | admin }.enable :edit_note + rule { can?(:master_access) }.enable :edit_note + + rule { is_author }.policy do + enable :read_note + enable :update_note + enable :admin_note + enable :resolve_note + end + + rule { for_merge_request & is_noteable_author }.policy do + enable :resolve_note end end diff --git a/app/policies/personal_snippet_policy.rb b/app/policies/personal_snippet_policy.rb index e1e5336da8c..cac0530b9f7 100644 --- a/app/policies/personal_snippet_policy.rb +++ b/app/policies/personal_snippet_policy.rb @@ -1,27 +1,28 @@ class PersonalSnippetPolicy < BasePolicy - def rules - can! :read_personal_snippet if @subject.public? - return unless @user + condition(:public_snippet, scope: :subject) { @subject.public? } + condition(:is_author) { @user && @subject.author == @user } + condition(:internal_snippet, scope: :subject) { @subject.internal? } - if @subject.public? - can! :comment_personal_snippet - end + rule { public_snippet }.policy do + enable :read_personal_snippet + enable :comment_personal_snippet + end - if @subject.author == @user - can! :read_personal_snippet - can! :update_personal_snippet - can! :destroy_personal_snippet - can! :admin_personal_snippet - can! :comment_personal_snippet - end + rule { is_author }.policy do + enable :read_personal_snippet + enable :update_personal_snippet + enable :destroy_personal_snippet + enable :admin_personal_snippet + enable :comment_personal_snippet + end - unless @user.external? - can! :create_personal_snippet - end + rule { ~anonymous }.enable :create_personal_snippet + rule { external_user }.prevent :create_personal_snippet - if @subject.internal? && !@user.external? - can! :read_personal_snippet - can! :comment_personal_snippet - end + rule { internal_snippet & ~external_user }.policy do + enable :read_personal_snippet + enable :comment_personal_snippet end + + rule { anonymous }.prevent :comment_personal_snippet end diff --git a/app/policies/project_label_policy.rb b/app/policies/project_label_policy.rb index b12b4c5166b..2d0f021118b 100644 --- a/app/policies/project_label_policy.rb +++ b/app/policies/project_label_policy.rb @@ -1,5 +1,3 @@ class ProjectLabelPolicy < BasePolicy - def rules - delegate! @subject.project - end + delegate { @subject.project } end diff --git a/app/policies/project_member_policy.rb b/app/policies/project_member_policy.rb index 1c038dddd4b..9aedb620be9 100644 --- a/app/policies/project_member_policy.rb +++ b/app/policies/project_member_policy.rb @@ -1,22 +1,16 @@ class ProjectMemberPolicy < BasePolicy - def rules - # anonymous users have no abilities here - return unless @user + delegate { @subject.project } - target_user = @subject.user - project = @subject.project + condition(:target_is_owner, scope: :subject) { @subject.user == @subject.project.owner } + condition(:target_is_self) { @user && @subject.user == @user } - return if target_user == project.owner + rule { anonymous }.prevent_all + rule { target_is_owner }.prevent_all - can_manage = Ability.allowed?(@user, :admin_project_member, project) - - if can_manage - can! :update_project_member - can! :destroy_project_member - end - - if @user == target_user - can! :destroy_project_member - end + rule { can?(:admin_project_member) }.policy do + enable :update_project_member + enable :destroy_project_member end + + rule { target_is_self }.enable :destroy_project_member end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 47518dddb61..7cbca63fab4 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -1,297 +1,353 @@ class ProjectPolicy < BasePolicy - def rules - team_access!(user) + def self.create_read_update_admin(name) + [ + :"create_#{name}", + :"read_#{name}", + :"update_#{name}", + :"admin_#{name}" + ] + end - owner_access! if user.admin? || owner? - team_member_owner_access! if owner? + desc "User is a project owner" + condition :owner do + @user && project.owner == @user || (project.group && project.group.has_owner?(@user)) + end - if project.public? || (project.internal? && !user.external?) - guest_access! - public_access! - can! :request_access if access_requestable? - end + desc "Project has public builds enabled" + condition(:public_builds, scope: :subject) { project.public_builds? } + + # For guest access we use #is_team_member? so we can use + # project.members, which gets cached in subject scope. + # This is safe because team_access_level is guaranteed + # by ProjectAuthorization's validation to be at minimum + # GUEST + desc "User has guest access" + condition(:guest) { is_team_member? } - archived_access! if project.archived? + desc "User has reporter access" + condition(:reporter) { team_access_level >= Gitlab::Access::REPORTER } - disabled_features! + desc "User has developer access" + condition(:developer) { team_access_level >= Gitlab::Access::DEVELOPER } + + desc "User has master access" + condition(:master) { team_access_level >= Gitlab::Access::MASTER } + + desc "Project is public" + condition(:public_project, scope: :subject) { project.public? } + + desc "Project is visible to internal users" + condition(:internal_access) do + project.internal? && !user.external? end - def project - @subject + desc "User is a member of the group" + condition(:group_member, scope: :subject) { project_group_member? } + + desc "Project is archived" + condition(:archived, scope: :subject) { project.archived? } + + condition(:default_issues_tracker, scope: :subject) { project.default_issues_tracker? } + + desc "Container registry is disabled" + condition(:container_registry_disabled, scope: :subject) do + !project.container_registry_enabled end - def owner? - return @owner if defined?(@owner) - - @owner = project.owner == user || - (project.group && project.group.has_owner?(user)) - end - - def guest_access! - can! :read_project - can! :read_board - can! :read_list - can! :read_wiki - can! :read_issue - can! :read_label - can! :read_milestone - can! :read_project_snippet - can! :read_project_member - can! :read_note - can! :create_project - can! :create_issue - can! :create_note - can! :upload_file - can! :read_cycle_analytics - - if project.public_builds? - can! :read_pipeline - can! :read_pipeline_schedule - can! :read_build - end + desc "Project has an external wiki" + condition(:has_external_wiki, scope: :subject) { project.has_external_wiki? } + + desc "Project has request access enabled" + condition(:request_access_enabled, scope: :subject) { project.request_access_enabled } + + features = %w[ + merge_requests + issues + repository + snippets + wiki + builds + ] + + features.each do |f| + # these are scored high because they are unlikely + desc "Project has #{f} disabled" + condition(:"#{f}_disabled", score: 32) { !feature_available?(f.to_sym) } end - def reporter_access! - can! :download_code - can! :download_wiki_code - can! :fork_project - can! :create_project_snippet - can! :update_issue - can! :admin_issue - can! :admin_label - can! :admin_list - can! :read_commit_status - can! :read_build - can! :read_container_image - can! :read_pipeline - can! :read_pipeline_schedule - can! :read_environment - can! :read_deployment - can! :read_merge_request - end - - # Permissions given when an user is team member of a project - def team_member_reporter_access! - can! :build_download_code - can! :build_read_container_image - end - - def developer_access! - can! :admin_merge_request - can! :update_merge_request - can! :create_commit_status - can! :update_commit_status - can! :create_build - can! :update_build - can! :create_pipeline - can! :update_pipeline - can! :create_pipeline_schedule - can! :update_pipeline_schedule - can! :create_merge_request - can! :create_wiki - can! :push_code - can! :resolve_note - can! :create_container_image - can! :update_container_image - can! :create_environment - can! :create_deployment - end - - def master_access! - can! :delete_protected_branch - can! :update_project_snippet - can! :update_environment - can! :update_deployment - can! :admin_milestone - can! :admin_project_snippet - can! :admin_project_member - can! :admin_note - can! :admin_wiki - can! :admin_project - can! :admin_commit_status - can! :admin_build - can! :admin_container_image - can! :admin_pipeline - can! :admin_pipeline_schedule - can! :admin_environment - can! :admin_deployment - can! :admin_pages - can! :read_pages - can! :update_pages - end - - def public_access! - can! :download_code - can! :fork_project - can! :read_commit_status - can! :read_pipeline - can! :read_pipeline_schedule - can! :read_container_image - can! :build_download_code - can! :build_read_container_image - can! :read_merge_request - end - - def owner_access! - guest_access! - reporter_access! - developer_access! - master_access! - can! :change_namespace - can! :change_visibility_level - can! :rename_project - can! :remove_project - can! :archive_project - can! :remove_fork_project - can! :destroy_merge_request - can! :destroy_issue - can! :remove_pages - end - - def team_member_owner_access! - team_member_reporter_access! - end - - # Push abilities on the users team role - def team_access!(user) - access = project.team.max_member_access(user.id) - - return if access < Gitlab::Access::GUEST - guest_access! - - return if access < Gitlab::Access::REPORTER - reporter_access! - team_member_reporter_access! - - return if access < Gitlab::Access::DEVELOPER - developer_access! - - return if access < Gitlab::Access::MASTER - master_access! - end - - def archived_access! - cannot! :create_merge_request - cannot! :push_code - cannot! :delete_protected_branch - cannot! :update_merge_request - cannot! :admin_merge_request - end - - def disabled_features! - repository_enabled = project.feature_available?(:repository, user) - - block_issues_abilities - - unless project.feature_available?(:merge_requests, user) && repository_enabled - cannot!(*named_abilities(:merge_request)) - end + rule { guest }.enable :guest_access + rule { reporter }.enable :reporter_access + rule { developer }.enable :developer_access + rule { master }.enable :master_access + + rule { owner | admin }.policy do + enable :guest_access + enable :reporter_access + enable :developer_access + enable :master_access + + enable :change_namespace + enable :change_visibility_level + enable :rename_project + enable :remove_project + enable :archive_project + enable :remove_fork_project + enable :destroy_merge_request + enable :destroy_issue + enable :remove_pages + end - unless project.feature_available?(:issues, user) || project.feature_available?(:merge_requests, user) - cannot!(*named_abilities(:label)) - cannot!(*named_abilities(:milestone)) - end + rule { owner | reporter }.policy do + enable :build_download_code + enable :build_read_container_image + end - unless project.feature_available?(:snippets, user) - cannot!(*named_abilities(:project_snippet)) - end + rule { can?(:guest_access) }.policy do + enable :read_project + enable :read_board + enable :read_list + enable :read_wiki + enable :read_issue + enable :read_label + enable :read_milestone + enable :read_project_snippet + enable :read_project_member + enable :read_note + enable :create_project + enable :create_issue + enable :create_note + enable :upload_file + enable :read_cycle_analytics + enable :read_project_snippet + end - unless project.feature_available?(:wiki, user) || project.has_external_wiki? - cannot!(*named_abilities(:wiki)) - cannot!(:download_wiki_code) - end + rule { can?(:reporter_access) }.policy do + enable :download_code + enable :download_wiki_code + enable :fork_project + enable :create_project_snippet + enable :update_issue + enable :admin_issue + enable :admin_label + enable :admin_list + enable :read_commit_status + enable :read_build + enable :read_container_image + enable :read_pipeline + enable :read_pipeline_schedule + enable :read_environment + enable :read_deployment + enable :read_merge_request + end - unless project.feature_available?(:builds, user) && repository_enabled - cannot!(*named_abilities(:build)) - cannot!(*named_abilities(:pipeline) - [:read_pipeline]) - cannot!(*named_abilities(:pipeline_schedule)) - cannot!(*named_abilities(:environment)) - cannot!(*named_abilities(:deployment)) - end + rule { (~anonymous & public_project) | internal_access }.policy do + enable :public_user_access + end - unless repository_enabled - cannot! :push_code - cannot! :delete_protected_branch - cannot! :download_code - cannot! :fork_project - cannot! :read_commit_status - end + rule { can?(:public_user_access) }.policy do + enable :guest_access + enable :request_access + end - unless project.container_registry_enabled - cannot!(*named_abilities(:container_image)) - end + rule { owner | admin | guest | group_member }.prevent :request_access + rule { ~request_access_enabled }.prevent :request_access + + rule { can?(:developer_access) }.policy do + enable :admin_merge_request + enable :update_merge_request + enable :create_commit_status + enable :update_commit_status + enable :create_build + enable :update_build + enable :create_pipeline + enable :update_pipeline + enable :create_pipeline_schedule + enable :update_pipeline_schedule + enable :create_merge_request + enable :create_wiki + enable :push_code + enable :resolve_note + enable :create_container_image + enable :update_container_image + enable :create_environment + enable :create_deployment end - def anonymous_rules - return unless project.public? + rule { can?(:master_access) }.policy do + enable :delete_protected_branch + enable :update_project_snippet + enable :update_environment + enable :update_deployment + enable :admin_milestone + enable :admin_project_snippet + enable :admin_project_member + enable :admin_note + enable :admin_wiki + enable :admin_project + enable :admin_commit_status + enable :admin_build + enable :admin_container_image + enable :admin_pipeline + enable :admin_pipeline_schedule + enable :admin_environment + enable :admin_deployment + enable :admin_pages + enable :read_pages + enable :update_pages + end - base_readonly_access! + rule { can?(:public_user_access) }.policy do + enable :public_access - # Allow to read builds by anonymous user if guests are allowed - can! :read_build if project.public_builds? + enable :fork_project + enable :build_download_code + enable :build_read_container_image + end - disabled_features! + rule { archived }.policy do + prevent :create_merge_request + prevent :push_code + prevent :delete_protected_branch + prevent :update_merge_request + prevent :admin_merge_request end - def block_issues_abilities - unless project.feature_available?(:issues, user) - cannot! :read_issue if project.default_issues_tracker? - cannot! :create_issue - cannot! :update_issue - cannot! :admin_issue - end + rule { merge_requests_disabled | repository_disabled }.policy do + prevent(*create_read_update_admin(:merge_request)) end - def named_abilities(name) - [ - :"read_#{name}", - :"create_#{name}", - :"update_#{name}", - :"admin_#{name}" - ] + rule { issues_disabled & merge_requests_disabled }.policy do + prevent(*create_read_update_admin(:label)) + prevent(*create_read_update_admin(:milestone)) + end + + rule { snippets_disabled }.policy do + prevent(*create_read_update_admin(:project_snippet)) + end + + rule { wiki_disabled & ~has_external_wiki }.policy do + prevent(*create_read_update_admin(:wiki)) + prevent(:download_wiki_code) + end + + rule { builds_disabled | repository_disabled }.policy do + prevent(*create_read_update_admin(:build)) + prevent(*(create_read_update_admin(:pipeline) - [:read_pipeline])) + prevent(*create_read_update_admin(:pipeline_schedule)) + prevent(*create_read_update_admin(:environment)) + prevent(*create_read_update_admin(:deployment)) + end + + rule { repository_disabled }.policy do + prevent :push_code + prevent :push_code_to_protected_branches + prevent :download_code + prevent :fork_project + prevent :read_commit_status + end + + rule { container_registry_disabled }.policy do + prevent(*create_read_update_admin(:container_image)) + end + + rule { anonymous & ~public_project }.prevent_all + rule { public_project }.enable(:public_access) + + rule { can?(:public_access) }.policy do + enable :read_project + enable :read_board + enable :read_list + enable :read_wiki + enable :read_label + enable :read_milestone + enable :read_project_snippet + enable :read_project_member + enable :read_merge_request + enable :read_note + enable :read_pipeline + enable :read_pipeline_schedule + enable :read_commit_status + enable :read_container_image + enable :download_code + enable :download_wiki_code + enable :read_cycle_analytics + + # NOTE: may be overridden by IssuePolicy + enable :read_issue + end + + rule { public_builds }.policy do + enable :read_build + end + + rule { public_builds & can?(:guest_access) }.policy do + enable :read_pipeline + enable :read_pipeline_schedule + end + + rule { issues_disabled }.policy do + prevent :create_issue + prevent :update_issue + prevent :admin_issue + end + + rule { issues_disabled & default_issues_tracker }.policy do + prevent :read_issue end private - def project_group_member?(user) + def is_team_member? + return false if @user.nil? + + greedy_load_subject = false + + # when scoping by subject, we want to be greedy + # and load *all* the members with one query. + greedy_load_subject ||= DeclarativePolicy.preferred_scope == :subject + + # in this case we're likely to have loaded #members already + # anyways, and #member? would fail with an error + greedy_load_subject ||= !@user.persisted? + + if greedy_load_subject + project.team.members.include?(user) + else + # otherwise we just make a specific query for + # this particular user. + team_access_level >= Gitlab::Access::GUEST + end + end + + def project_group_member? + return false if @user.nil? + project.group && ( - project.group.members_with_parents.exists?(user_id: user.id) || - project.group.requesters.exists?(user_id: user.id) + project.group.members_with_parents.exists?(user_id: @user.id) || + project.group.requesters.exists?(user_id: @user.id) ) end - def access_requestable? - project.request_access_enabled && - !owner? && - !user.admin? && - !project.team.member?(user) && - !project_group_member?(user) - end - - # A base set of abilities for read-only users, which - # is then augmented as necessary for anonymous and other - # read-only users. - def base_readonly_access! - can! :read_project - can! :read_board - can! :read_list - can! :read_wiki - can! :read_label - can! :read_milestone - can! :read_project_snippet - can! :read_project_member - can! :read_merge_request - can! :read_note - can! :read_pipeline - can! :read_pipeline_schedule - can! :read_commit_status - can! :read_container_image - can! :download_code - can! :download_wiki_code - can! :read_cycle_analytics + def team_access_level + return -1 if @user.nil? - # NOTE: may be overridden by IssuePolicy - can! :read_issue + # NOTE: max_member_access has its own cache + project.team.max_member_access(@user.id) + end + + def feature_available?(feature) + case project.project_feature.access_level(feature) + when ProjectFeature::DISABLED + false + when ProjectFeature::PRIVATE + guest? || admin? + else + true + end + end + + def project + @subject end end diff --git a/app/policies/project_snippet_policy.rb b/app/policies/project_snippet_policy.rb index bc5c4f32f79..dd270643bbf 100644 --- a/app/policies/project_snippet_policy.rb +++ b/app/policies/project_snippet_policy.rb @@ -1,25 +1,45 @@ class ProjectSnippetPolicy < BasePolicy - def rules - # We have to check both project feature visibility and a snippet visibility and take the stricter one - # This will be simplified - check https://gitlab.com/gitlab-org/gitlab-ce/issues/27573 - return unless @subject.project.feature_available?(:snippets, @user) - return unless Ability.allowed?(@user, :read_project, @subject.project) - - can! :read_project_snippet if @subject.public? - return unless @user - - if @user && (@subject.author == @user || @user.admin?) - can! :read_project_snippet - can! :update_project_snippet - can! :admin_project_snippet - end - - if @subject.internal? && !@user.external? - can! :read_project_snippet - end - - if @subject.project.team.member?(@user) - can! :read_project_snippet - end + delegate :project + + desc "Snippet is public" + condition(:public_snippet, scope: :subject) { @subject.public? } + condition(:private_snippet, scope: :subject) { @subject.private? } + condition(:public_project, scope: :subject) { @subject.project.public? } + + condition(:is_author) { @user && @subject.author == @user } + + condition(:internal, scope: :subject) { @subject.internal? } + + # We have to check both project feature visibility and a snippet visibility and take the stricter one + # This will be simplified - check https://gitlab.com/gitlab-org/gitlab-ce/issues/27573 + rule { ~can?(:read_project) }.policy do + prevent :read_project_snippet + prevent :update_project_snippet + prevent :admin_project_snippet + end + + # we have to use this complicated prevent because the delegated project policy + # is overly greedy in allowing :read_project_snippet, since it doesn't have any + # information about the snippet. However, :read_project_snippet on the *project* + # is used to hide/show various snippet-related controls, so we can't just move + # all of the handling here. + rule do + all?(private_snippet | (internal & external_user), + ~project.guest, + ~admin, + ~is_author) + end.prevent :read_project_snippet + + rule { internal & ~is_author & ~admin }.policy do + prevent :update_project_snippet + prevent :admin_project_snippet + end + + rule { public_snippet }.enable :read_project_snippet + + rule { is_author | admin }.policy do + enable :read_project_snippet + enable :update_project_snippet + enable :admin_project_snippet end end diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb index 229846e368c..0181ddf85e0 100644 --- a/app/policies/user_policy.rb +++ b/app/policies/user_policy.rb @@ -1,19 +1,20 @@ class UserPolicy < BasePolicy include Gitlab::CurrentSettings - def rules - can! :read_user if @user || !restricted_public_level? + desc "The application is restricted from public visibility" + condition(:restricted_public_level, scope: :global) do + current_application_settings.restricted_visibility_levels.include?(Gitlab::VisibilityLevel::PUBLIC) + end - if @user - if @user.admin? || @subject == @user - can! :destroy_user - end + desc "The current user is the user in question" + condition(:user_is_self, score: 0) { @subject == @user } - cannot! :destroy_user if @subject.ghost? - end - end + desc "This is the ghost user" + condition(:subject_ghost, scope: :subject, score: 0) { @subject.ghost? } - def restricted_public_level? - current_application_settings.restricted_visibility_levels.include?(Gitlab::VisibilityLevel::PUBLIC) - end + rule { ~restricted_public_level }.enable :read_user + rule { ~anonymous }.enable :read_user + + rule { user_is_self | admin }.enable :destroy_user + rule { subject_ghost }.prevent :destroy_user end diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb index af84d4c7427..b951e8d0c9f 100644 --- a/app/services/ci/register_job_service.rb +++ b/app/services/ci/register_job_service.rb @@ -54,7 +54,7 @@ module Ci def builds_for_shared_runner new_builds. # don't run projects which have not enabled shared runners and builds - joins(:project).where(projects: { shared_runners_enabled: true }) + joins(:project).where(projects: { shared_runners_enabled: true, pending_delete: false }) .joins('LEFT JOIN project_features ON ci_builds.project_id = project_features.project_id') .where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0'). @@ -66,7 +66,7 @@ module Ci end def builds_for_specific_runner - new_builds.where(project: runner.projects.with_builds_enabled).order('created_at ASC') + new_builds.where(project: runner.projects.without_deleted.with_builds_enabled).order('created_at ASC') end def running_builds_for_shared_runners diff --git a/app/services/groups/destroy_service.rb b/app/services/groups/destroy_service.rb index 497fdb09cdc..d40d280140a 100644 --- a/app/services/groups/destroy_service.rb +++ b/app/services/groups/destroy_service.rb @@ -10,7 +10,7 @@ module Groups def execute group.prepare_for_destroy - group.projects.with_deleted.each do |project| + group.projects.each do |project| # Execute the destruction of the models immediately to ensure atomic cleanup. # Skip repository removal because we remove directory with namespace # that contain all these repositories diff --git a/app/services/merge_requests/get_urls_service.rb b/app/services/merge_requests/get_urls_service.rb index f00a33969a8..5dd40e07c0d 100644 --- a/app/services/merge_requests/get_urls_service.rb +++ b/app/services/merge_requests/get_urls_service.rb @@ -49,7 +49,7 @@ module MergeRequests def url_for_new_merge_request(branch_name) merge_request_params = { source_branch: branch_name } - url = Gitlab::Routing.url_helpers.new_namespace_project_merge_request_url(project.namespace, project, merge_request: merge_request_params) + url = Gitlab::Routing.url_helpers.namespace_project_new_merge_request_url(project.namespace, project, merge_request: merge_request_params) { branch_name: branch_name, url: url, new_merge_request: true } end diff --git a/app/services/projects/unlink_fork_service.rb b/app/services/projects/unlink_fork_service.rb index 315c3e16292..f385e426827 100644 --- a/app/services/projects/unlink_fork_service.rb +++ b/app/services/projects/unlink_fork_service.rb @@ -10,7 +10,7 @@ module Projects merge_requests = @project.forked_from_project.merge_requests.opened.from_project(@project) merge_requests.each do |mr| - MergeRequests::CloseService.new(@project, @current_user).execute(mr) + ::MergeRequests::CloseService.new(@project, @current_user).execute(mr) end @project.forked_project_link.destroy diff --git a/app/services/users/destroy_service.rb b/app/services/users/destroy_service.rb index 673afb8b5b9..9d7237c2fbb 100644 --- a/app/services/users/destroy_service.rb +++ b/app/services/users/destroy_service.rb @@ -35,7 +35,7 @@ module Users Groups::DestroyService.new(group, current_user).execute end - user.personal_projects.with_deleted.each do |project| + user.personal_projects.each do |project| # Skip repository removal because we remove directory with namespace # that contain all this repositories ::Projects::DestroyService.new(project, current_user, skip_repo: true).execute diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index eabc9a3b01c..cc710f4ec7d 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -11,6 +11,8 @@ %meta{ property: 'og:title', content: page_title } %meta{ property: 'og:description', content: page_description } %meta{ property: 'og:image', content: page_image } + %meta{ property: 'og:image:width', content: '64' } + %meta{ property: 'og:image:height', content: '64' } %meta{ property: 'og:url', content: request.base_url + request.fullpath } -# Twitter Card - https://dev.twitter.com/cards/types/summary diff --git a/app/views/layouts/header/_new_dropdown.haml b/app/views/layouts/header/_new_dropdown.haml index 9ff1164f2ee..4d41579168c 100644 --- a/app/views/layouts/header/_new_dropdown.haml +++ b/app/views/layouts/header/_new_dropdown.haml @@ -33,7 +33,7 @@ = link_to 'New issue', new_namespace_project_issue_path(@project.namespace, @project) - if merge_project %li - = link_to 'New merge request', new_namespace_project_merge_request_path(merge_project.namespace, merge_project) + = link_to 'New merge request', namespace_project_new_merge_request_path(merge_project.namespace, merge_project) - if create_project_snippet %li.header-new-project-snippet = link_to 'New snippet', new_namespace_project_snippet_path(@project.namespace, @project) diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 29658da7792..68024d782a6 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -31,7 +31,9 @@ %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: @project.default_issues_tracker? ? :merge_requests : [:merge_requests, :labels, :milestones]) do + - controllers = [:merge_requests, 'projects/merge_requests/conflicts'] + - controllers.push(:merge_requests, :labels, :milestones) unless @project.default_issues_tracker? + = nav_link(controller: controllers) do = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do %span Merge Requests diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml index 960b57a8008..aa1a533b5cb 100644 --- a/app/views/projects/buttons/_dropdown.html.haml +++ b/app/views/projects/buttons/_dropdown.html.haml @@ -16,7 +16,7 @@ - if merge_project %li - = link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project) do + = link_to namespace_project_new_merge_request_path(merge_project.namespace, merge_project) do = icon('tasks fw') #{ _('New merge request') } diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml index 93fd0789c11..cf8dffc8957 100644 --- a/app/views/projects/commits/_commits.html.haml +++ b/app/views/projects/commits/_commits.html.haml @@ -8,7 +8,7 @@ %li.commits-row{ data: { day: day } } %ul.content-list.commit-list - = render commits, project: project, ref: ref + = render partial: 'projects/commits/commit', collection: commits, locals: { project: project, ref: ref } - if hidden > 0 %li.alert.alert-warning diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index d538c4c86c8..f9385459a66 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -10,7 +10,7 @@ - if show_whitespace_toggle - if current_controller?(:commit) = commit_diff_whitespace_link(diffs.project, @commit, class: 'hidden-xs') - - elsif current_controller?(:merge_requests) + - elsif current_controller?('projects/merge_requests/diffs') = diff_merge_request_whitespace_link(diffs.project, @merge_request, class: 'hidden-xs') - elsif current_controller?(:compare) = diff_compare_whitespace_link(diffs.project, params[:from], params[:to], class: 'hidden-xs') diff --git a/app/views/projects/diffs/_warning.html.haml b/app/views/projects/diffs/_warning.html.haml index 295a1b62535..402c18c447e 100644 --- a/app/views/projects/diffs/_warning.html.haml +++ b/app/views/projects/diffs/_warning.html.haml @@ -2,13 +2,12 @@ %h4 Too many changes to show. .pull-right - - if current_controller?(:commit) or current_controller?(:merge_requests) - - if current_controller?(:commit) - = link_to "Plain diff", namespace_project_commit_path(@project.namespace, @project, @commit, format: :diff), class: "btn btn-sm" - = link_to "Email patch", namespace_project_commit_path(@project.namespace, @project, @commit, format: :patch), class: "btn btn-sm" - - elsif @merge_request && @merge_request.persisted? - = link_to "Plain diff", merge_request_path(@merge_request, format: :diff), class: "btn btn-sm" - = link_to "Email patch", merge_request_path(@merge_request, format: :patch), class: "btn btn-sm" + - if current_controller?(:commit) + = link_to "Plain diff", namespace_project_commit_path(@project.namespace, @project, @commit, format: :diff), class: "btn btn-sm" + = link_to "Email patch", namespace_project_commit_path(@project.namespace, @project, @commit, format: :patch), class: "btn btn-sm" + - elsif current_controller?('projects/merge_requests/diffs') && @merge_request&.persisted? + = link_to "Plain diff", merge_request_path(@merge_request, format: :diff), class: "btn btn-sm" + = link_to "Email patch", merge_request_path(@merge_request, format: :patch), class: "btn btn-sm" %p To preserve performance only %strong #{diff_files.size} of #{diff_files.real_size} diff --git a/app/views/projects/merge_requests/show/_commits.html.haml b/app/views/projects/merge_requests/_commits.html.haml index 11793919ff7..11793919ff7 100644 --- a/app/views/projects/merge_requests/show/_commits.html.haml +++ b/app/views/projects/merge_requests/_commits.html.haml diff --git a/app/views/projects/merge_requests/show/_how_to_merge.html.haml b/app/views/projects/merge_requests/_how_to_merge.html.haml index 766cb272bec..766cb272bec 100644 --- a/app/views/projects/merge_requests/show/_how_to_merge.html.haml +++ b/app/views/projects/merge_requests/_how_to_merge.html.haml diff --git a/app/views/projects/merge_requests/show/_mr_box.html.haml b/app/views/projects/merge_requests/_mr_box.html.haml index 8a390cf8700..8a390cf8700 100644 --- a/app/views/projects/merge_requests/show/_mr_box.html.haml +++ b/app/views/projects/merge_requests/_mr_box.html.haml diff --git a/app/views/projects/merge_requests/show/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml index d9428b8562e..d9428b8562e 100644 --- a/app/views/projects/merge_requests/show/_mr_title.html.haml +++ b/app/views/projects/merge_requests/_mr_title.html.haml diff --git a/app/views/projects/merge_requests/show/_pipelines.html.haml b/app/views/projects/merge_requests/_pipelines.html.haml index 2f1dbe87619..2f1dbe87619 100644 --- a/app/views/projects/merge_requests/show/_pipelines.html.haml +++ b/app/views/projects/merge_requests/_pipelines.html.haml diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml deleted file mode 100644 index 75120409bb3..00000000000 --- a/app/views/projects/merge_requests/_show.html.haml +++ /dev/null @@ -1,97 +0,0 @@ -- @content_class = "limit-container-width" unless fluid_layout -- page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests" -- page_description @merge_request.description -- page_card_attributes @merge_request.card_attributes -- content_for :page_specific_javascripts do - = page_specific_javascript_bundle_tag('common_vue') - = page_specific_javascript_bundle_tag('diff_notes') - -.merge-request{ 'data-url' => merge_request_path(@merge_request, format: :json), 'data-project-path' => project_path(@merge_request.project) } - = render "projects/merge_requests/show/mr_title" - - .merge-request-details.issuable-details{ data: { id: @merge_request.project.id } } - = render "projects/merge_requests/show/mr_box" - - - if @merge_request.source_branch_exists? - = render "projects/merge_requests/show/how_to_merge" - - :javascript - window.gl.mrWidgetData = #{serialize_issuable(@merge_request)} - - #js-vue-mr-widget.mr-widget - - - content_for :page_specific_javascripts do - = webpack_bundle_tag 'common_vue' - = webpack_bundle_tag 'vue_merge_request_widget' - - .content-block.content-block-small.emoji-list-container - = render 'award_emoji/awards_block', awardable: @merge_request, inline: true - - .merge-request-tabs-holder{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') } - .merge-request-tabs-container - .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller - .fade-left= icon('angle-left') - .fade-right= icon('angle-right') - .nav-links.scrolling-tabs - %ul.merge-request-tabs - %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.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 - Commits - %span.badge= @commits_count - - if @pipelines.any? - %li.pipelines-tab - = link_to pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#pipelines', action: 'pipelines', toggle: 'tab' } do - Pipelines - %span.badge= @pipelines.size - %li.diffs-tab - = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#diffs', action: 'diffs', toggle: 'tab' } do - Changes - %span.badge= @merge_request.diff_size - #resolve-count-app.line-resolve-all-container.prepend-top-10{ "v-cloak" => true } - %resolve-count{ "inline-template" => true, ":logged-out" => "#{current_user.nil?}" } - %div - .line-resolve-all{ "v-show" => "discussionCount > 0", - ":class" => "{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }" } - %span.line-resolve-btn.is-disabled{ type: "button", - ":class" => "{ 'is-active': resolvedDiscussionCount === discussionCount }" } - = render "shared/icons/icon_status_success.svg" - %span.line-resolve-text - {{ resolvedDiscussionCount }}/{{ discussionCount }} {{ resolvedCountText }} resolved - = render "discussions/new_issue_for_all_discussions", merge_request: @merge_request - = render "discussions/jump_to_next" - - .tab-content#diff-notes-app - #notes.notes.tab-pane.voting_notes - .row - %section.col-md-12 - .issuable-discussion - = render "projects/merge_requests/discussion" - - #commits.commits.tab-pane - -# This tab is always loaded via AJAX - #pipelines.pipelines.tab-pane - - if @pipelines.any? - = render 'projects/commit/pipelines_list', disable_initialization: true, endpoint: pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request) - #diffs.diffs.tab-pane - -# This tab is always loaded via AJAX - - .mr-loading-status - = spinner - -= render 'shared/issuable/sidebar', issuable: @merge_request -- if @merge_request.can_be_reverted?(current_user) - = render "projects/commit/change", type: 'revert', commit: @merge_request.merge_commit, title: @merge_request.title -- if @merge_request.can_be_cherry_picked? - = render "projects/commit/change", type: 'cherry-pick', commit: @merge_request.merge_commit, title: @merge_request.title - -:javascript - $(function () { - window.mergeRequest = new MergeRequest({ - action: "#{controller.action_name}" - }); - }); diff --git a/app/views/projects/merge_requests/conflicts.html.haml b/app/views/projects/merge_requests/conflicts.html.haml index 51d59280be8..f016b9c13b3 100644 --- a/app/views/projects/merge_requests/conflicts.html.haml +++ b/app/views/projects/merge_requests/conflicts.html.haml @@ -3,10 +3,10 @@ = page_specific_javascript_bundle_tag('common_vue') = page_specific_javascript_bundle_tag('merge_conflicts') = page_specific_javascript_tag('lib/ace.js') -= render "projects/merge_requests/show/mr_title" += render "projects/merge_requests/mr_title" .merge-request-details.issuable-details - = render "projects/merge_requests/show/mr_box" + = render "projects/merge_requests/mr_box" = render 'shared/issuable/sidebar', issuable: @merge_request diff --git a/app/views/projects/merge_requests/conflicts/show.html.haml b/app/views/projects/merge_requests/conflicts/show.html.haml new file mode 100644 index 00000000000..f016b9c13b3 --- /dev/null +++ b/app/views/projects/merge_requests/conflicts/show.html.haml @@ -0,0 +1,38 @@ +- page_title "Merge Conflicts", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests" +- content_for :page_specific_javascripts do + = page_specific_javascript_bundle_tag('common_vue') + = page_specific_javascript_bundle_tag('merge_conflicts') + = page_specific_javascript_tag('lib/ace.js') += render "projects/merge_requests/mr_title" + +.merge-request-details.issuable-details + = render "projects/merge_requests/mr_box" + += render 'shared/issuable/sidebar', issuable: @merge_request + +#conflicts{ "v-cloak" => "true", data: { conflicts_path: conflicts_namespace_project_merge_request_path(@merge_request.project.namespace, @merge_request.project, @merge_request, format: :json), + resolve_conflicts_path: resolve_conflicts_namespace_project_merge_request_path(@merge_request.project.namespace, @merge_request.project, @merge_request) } } + .loading{ "v-if" => "isLoading" } + %i.fa.fa-spinner.fa-spin + + .nothing-here-block{ "v-if" => "hasError" } + {{conflictsData.errorMessage}} + + = render partial: "projects/merge_requests/conflicts/commit_stats" + + .files-wrapper{ "v-if" => "!isLoading && !hasError" } + .files + .diff-file.file-holder.conflict{ "v-for" => "file in conflictsData.files" } + .js-file-title.file-title + %i.fa.fa-fw{ ":class" => "file.iconClass" } + %strong {{file.filePath}} + = render partial: 'projects/merge_requests/conflicts/file_actions' + .diff-content.diff-wrap-lines + .diff-wrap-lines.code.file-content.js-syntax-highlight{ "v-show" => "!isParallel && file.resolveMode === 'interactive' && file.type === 'text'" } + = render partial: "projects/merge_requests/conflicts/components/inline_conflict_lines" + .diff-wrap-lines.code.file-content.js-syntax-highlight{ "v-show" => "isParallel && file.resolveMode === 'interactive' && file.type === 'text'" } + %parallel-conflict-lines{ ":file" => "file" } + %div{ "v-show" => "file.resolveMode === 'edit' || file.type === 'text-editor'" } + = render partial: "projects/merge_requests/conflicts/components/diff_file_editor" + + = render partial: "projects/merge_requests/conflicts/submit_form" diff --git a/app/views/projects/merge_requests/_new_diffs.html.haml b/app/views/projects/merge_requests/creations/_diffs.html.haml index 627fc4e9671..627fc4e9671 100644 --- a/app/views/projects/merge_requests/_new_diffs.html.haml +++ b/app/views/projects/merge_requests/creations/_diffs.html.haml diff --git a/app/views/projects/merge_requests/_new_compare.html.haml b/app/views/projects/merge_requests/creations/_new_compare.html.haml index 0f37abb579c..7cda326afef 100644 --- a/app/views/projects/merge_requests/_new_compare.html.haml +++ b/app/views/projects/merge_requests/creations/_new_compare.html.haml @@ -1,7 +1,7 @@ %h3.page-title New Merge Request -= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], url: new_namespace_project_merge_request_path(@project.namespace, @project), method: :get, html: { class: "merge-request-form form-inline js-requires-input" } do |f| += form_for [@project.namespace.becomes(Namespace), @project, @merge_request], url: namespace_project_new_merge_request_path(@project.namespace, @project), method: :get, html: { class: "merge-request-form form-inline js-requires-input" } do |f| .hide.alert.alert-danger.mr-compare-errors .merge-request-branches.row .col-md-6 @@ -69,7 +69,7 @@ :javascript new Compare({ - targetProjectUrl: "#{update_branches_namespace_project_merge_requests_path(@source_project.namespace, @source_project)}", - sourceBranchUrl: "#{branch_from_namespace_project_merge_requests_path(@source_project.namespace, @source_project)}", - targetBranchUrl: "#{branch_to_namespace_project_merge_requests_path(@source_project.namespace, @source_project)}" + targetProjectUrl: "#{namespace_project_new_merge_request_update_branches_path(@source_project.namespace, @source_project)}", + sourceBranchUrl: "#{namespace_project_new_merge_request_branch_from_path(@source_project.namespace, @source_project)}", + targetBranchUrl: "#{namespace_project_new_merge_request_branch_to_path(@source_project.namespace, @source_project)}" }); diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/creations/_new_submit.html.haml index e3ecbee5490..c72dd1d8e29 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/creations/_new_submit.html.haml @@ -31,28 +31,27 @@ %span.badge= @commits.size - if @pipelines.any? %li.builds-tab - = link_to url_for(params), data: {target: 'div#pipelines', action: 'pipelines', toggle: 'tab'} do + = link_to url_for(params.merge(action: 'pipelines')), data: {target: 'div#pipelines', action: 'pipelines', toggle: 'tab'} do Pipelines %span.badge= @pipelines.size %li.diffs-tab - = link_to url_for(params.merge(action: 'new_diffs')), data: {target: 'div#diffs', action: 'new/diffs', toggle: 'tab'} do + = link_to url_for(params.merge(action: 'diffs')), data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'} do Changes %span.badge= @merge_request.diff_size .tab-content #commits.commits.tab-pane.active - = render "projects/merge_requests/show/commits" + = render "projects/merge_requests/commits" #diffs.diffs.tab-pane -# This tab is always loaded via AJAX - if @pipelines.any? #pipelines.pipelines.tab-pane - = render 'projects/merge_requests/show/pipelines', endpoint: url_for(params.merge(format: :json)), disable_initialization: true + = render 'projects/merge_requests/pipelines', endpoint: url_for(params.merge(action: 'pipelines', format: :json)), disable_initialization: true .mr-loading-status = spinner :javascript var merge_request = new MergeRequest({ - action: "#{(@show_changes_tab ? 'new/diffs' : 'new')}", - setUrl: false, + action: "#{j params[:tab].presence || 'new'}", }); diff --git a/app/views/projects/merge_requests/branch_from.html.haml b/app/views/projects/merge_requests/creations/branch_from.html.haml index 3837c4b388d..3837c4b388d 100644 --- a/app/views/projects/merge_requests/branch_from.html.haml +++ b/app/views/projects/merge_requests/creations/branch_from.html.haml diff --git a/app/views/projects/merge_requests/branch_to.html.haml b/app/views/projects/merge_requests/creations/branch_to.html.haml index d69b71790a0..d69b71790a0 100644 --- a/app/views/projects/merge_requests/branch_to.html.haml +++ b/app/views/projects/merge_requests/creations/branch_to.html.haml diff --git a/app/views/projects/merge_requests/new.html.haml b/app/views/projects/merge_requests/creations/new.html.haml index 2e798ce780a..2e798ce780a 100644 --- a/app/views/projects/merge_requests/new.html.haml +++ b/app/views/projects/merge_requests/creations/new.html.haml diff --git a/app/views/projects/merge_requests/update_branches.html.haml b/app/views/projects/merge_requests/creations/update_branches.html.haml index 64482973a89..64482973a89 100644 --- a/app/views/projects/merge_requests/update_branches.html.haml +++ b/app/views/projects/merge_requests/creations/update_branches.html.haml diff --git a/app/views/projects/merge_requests/diffs.html.haml b/app/views/projects/merge_requests/diffs.html.haml deleted file mode 100644 index 2a5b8b1441e..00000000000 --- a/app/views/projects/merge_requests/diffs.html.haml +++ /dev/null @@ -1 +0,0 @@ -= render "show" diff --git a/app/views/projects/merge_requests/show/_diffs.html.haml b/app/views/projects/merge_requests/diffs/_diffs.html.haml index 7f0913ea516..fb31e2fef00 100644 --- a/app/views/projects/merge_requests/show/_diffs.html.haml +++ b/app/views/projects/merge_requests/diffs/_diffs.html.haml @@ -1,5 +1,5 @@ - if @merge_request_diff.collected? || @merge_request_diff.overflow? - = render 'projects/merge_requests/show/versions' + = render 'projects/merge_requests/diffs/versions' = render "projects/diffs/diffs", diffs: @diffs, environment: @environment - elsif @merge_request_diff.empty? .nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch} diff --git a/app/views/projects/merge_requests/show/_versions.html.haml b/app/views/projects/merge_requests/diffs/_versions.html.haml index 0999b95c9c9..0999b95c9c9 100644 --- a/app/views/projects/merge_requests/show/_versions.html.haml +++ b/app/views/projects/merge_requests/diffs/_versions.html.haml diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml index 6d75a9f34a3..86996e488a1 100644 --- a/app/views/projects/merge_requests/index.html.haml +++ b/app/views/projects/merge_requests/index.html.haml @@ -22,7 +22,7 @@ = button_tag "Edit Merge Requests", class: "btn js-bulk-update-toggle" - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) - if merge_project - = link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project), class: "btn btn-new", title: "New merge request" do + = link_to namespace_project_new_merge_request_path(merge_project.namespace, merge_project), class: "btn btn-new", title: "New merge request" do New merge request = render 'shared/issuable/search_bar', type: :merge_requests @@ -33,4 +33,4 @@ .merge-requests-holder = render 'merge_requests' - else - = render 'shared/empty_states/merge_requests', button_path: new_namespace_project_merge_request_path(@project.namespace, @project) + = render 'shared/empty_states/merge_requests', button_path: namespace_project_new_merge_request_path(@project.namespace, @project) diff --git a/app/views/projects/merge_requests/invalid.html.haml b/app/views/projects/merge_requests/invalid.html.haml index a00d3128ffe..6df19d6438b 100644 --- a/app/views/projects/merge_requests/invalid.html.haml +++ b/app/views/projects/merge_requests/invalid.html.haml @@ -1,8 +1,8 @@ - page_title "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests" .merge-request - = render "projects/merge_requests/show/mr_title" - = render "projects/merge_requests/show/mr_box" + = render "projects/merge_requests/mr_title" + = render "projects/merge_requests/mr_box" .alert.alert-danger %p diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml index 2a5b8b1441e..dbbf1bde088 100644 --- a/app/views/projects/merge_requests/show.html.haml +++ b/app/views/projects/merge_requests/show.html.haml @@ -1 +1,97 @@ -= render "show" +- @content_class = "limit-container-width" unless fluid_layout +- page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests" +- page_description @merge_request.description +- page_card_attributes @merge_request.card_attributes +- content_for :page_specific_javascripts do + = page_specific_javascript_bundle_tag('common_vue') + = page_specific_javascript_bundle_tag('diff_notes') + +.merge-request{ 'data-url' => merge_request_path(@merge_request, format: :json), 'data-project-path' => project_path(@merge_request.project) } + = render "projects/merge_requests/mr_title" + + .merge-request-details.issuable-details{ data: { id: @merge_request.project.id } } + = render "projects/merge_requests/mr_box" + + - if @merge_request.source_branch_exists? + = render "projects/merge_requests/how_to_merge" + + :javascript + window.gl.mrWidgetData = #{serialize_issuable(@merge_request)} + + #js-vue-mr-widget.mr-widget + + - content_for :page_specific_javascripts do + = webpack_bundle_tag 'common_vue' + = webpack_bundle_tag 'vue_merge_request_widget' + + .content-block.content-block-small.emoji-list-container + = render 'award_emoji/awards_block', awardable: @merge_request, inline: true + + .merge-request-tabs-holder{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') } + .merge-request-tabs-container + .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller + .fade-left= icon('angle-left') + .fade-right= icon('angle-right') + .nav-links.scrolling-tabs + %ul.merge-request-tabs + %li.notes-tab + = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#notes', action: 'show', toggle: 'tab' } do + Discussion + %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 + Commits + %span.badge= @commits_count + - if @pipelines.any? + %li.pipelines-tab + = link_to pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#pipelines', action: 'pipelines', toggle: 'tab' } do + Pipelines + %span.badge= @pipelines.size + %li.diffs-tab + = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#diffs', action: 'diffs', toggle: 'tab' } do + Changes + %span.badge= @merge_request.diff_size + #resolve-count-app.line-resolve-all-container.prepend-top-10{ "v-cloak" => true } + %resolve-count{ "inline-template" => true, ":logged-out" => "#{current_user.nil?}" } + %div + .line-resolve-all{ "v-show" => "discussionCount > 0", + ":class" => "{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }" } + %span.line-resolve-btn.is-disabled{ type: "button", + ":class" => "{ 'is-active': resolvedDiscussionCount === discussionCount }" } + = render "shared/icons/icon_status_success.svg" + %span.line-resolve-text + {{ resolvedDiscussionCount }}/{{ discussionCount }} {{ resolvedCountText }} resolved + = render "discussions/new_issue_for_all_discussions", merge_request: @merge_request + = render "discussions/jump_to_next" + + .tab-content#diff-notes-app + #notes.notes.tab-pane.voting_notes + .row + %section.col-md-12 + .issuable-discussion + = render "projects/merge_requests/discussion" + + #commits.commits.tab-pane + -# This tab is always loaded via AJAX + #pipelines.pipelines.tab-pane + - if @pipelines.any? + = render 'projects/commit/pipelines_list', disable_initialization: true, endpoint: pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request) + #diffs.diffs.tab-pane + -# This tab is always loaded via AJAX + + .mr-loading-status + = spinner + += render 'shared/issuable/sidebar', issuable: @merge_request +- if @merge_request.can_be_reverted?(current_user) + = render "projects/commit/change", type: 'revert', commit: @merge_request.merge_commit, title: @merge_request.title +- if @merge_request.can_be_cherry_picked? + = render "projects/commit/change", type: 'cherry-pick', commit: @merge_request.merge_commit, title: @merge_request.title + +:javascript + $(function () { + window.mergeRequest = new MergeRequest({ + action: "#{j params[:tab].presence || 'show'}", + }); + }); diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 7447197ed89..e1e70a53709 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -21,7 +21,7 @@ %li = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do #{n_('Commit', 'Commits', @project.statistics.commit_count)} (#{number_with_delimiter(@project.statistics.commit_count)}) - %l + %li = link_to namespace_project_branches_path(@project.namespace, @project) do #{n_('Branch', 'Branches', @repository.branch_count)} (#{number_with_delimiter(@repository.branch_count)}) %li diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml index aaffc0927eb..7ed6c622558 100644 --- a/app/views/shared/projects/_list.html.haml +++ b/app/views/shared/projects/_list.html.haml @@ -13,7 +13,7 @@ - if projects.any? %ul.projects-list - projects.each_with_index do |project, i| - - css_class = (i >= projects_limit) ? 'hide' : nil + - css_class = (i >= projects_limit) || project.pending_delete? ? 'hide' : nil = render "shared/projects/project", project: project, skip_namespace: skip_namespace, avatar: avatar, stars: stars, css_class: css_class, ci: ci, use_creator_avatar: use_creator_avatar, forks: forks, show_last_commit_as_description: show_last_commit_as_description diff --git a/app/workers/expire_pipeline_cache_worker.rb b/app/workers/expire_pipeline_cache_worker.rb index d760f5b140f..92e622285de 100644 --- a/app/workers/expire_pipeline_cache_worker.rb +++ b/app/workers/expire_pipeline_cache_worker.rb @@ -46,7 +46,7 @@ class ExpirePipelineCacheWorker end def new_merge_request_pipelines_path(project) - Gitlab::Routing.url_helpers.new_namespace_project_merge_request_path( + Gitlab::Routing.url_helpers.namespace_project_new_merge_request_path( project.namespace, project, format: :json) diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb index 89286595ca6..b8f8d3750d9 100644 --- a/app/workers/post_receive.rb +++ b/app/workers/post_receive.rb @@ -2,11 +2,11 @@ class PostReceive include Sidekiq::Worker include DedicatedSidekiqQueue - def perform(project_identifier, identifier, changes) - project, is_wiki = parse_project_identifier(project_identifier) + def perform(gl_repository, identifier, changes) + project, is_wiki = Gitlab::GlRepository.parse(gl_repository) if project.nil? - log("Triggered hook for non-existing project with identifier \"#{project_identifier}\"") + log("Triggered hook for non-existing project with gl_repository \"#{gl_repository}\"") return false end @@ -59,21 +59,6 @@ class PostReceive # Nothing defined here yet. end - # To maintain backwards compatibility, we accept both gl_repository or - # repository paths as project identifiers. Our plan is to migrate to - # gl_repository only with the following plan: - # 9.2: Handle both possible values. Keep Gitlab-Shell sending only repo paths - # 9.3 (or patch release): Make GitLab Shell pass gl_repository if present - # 9.4 (or patch release): Make GitLab Shell always pass gl_repository - # 9.5 (or patch release): Handle only gl_repository as project identifier on this method - def parse_project_identifier(project_identifier) - if project_identifier.start_with?('/') - Gitlab::RepoPath.parse(project_identifier) - else - Gitlab::GlRepository.parse(project_identifier) - end - end - def log(message) Gitlab::GitLogger.error("POST-RECEIVE: #{message}") end diff --git a/app/workers/stuck_ci_jobs_worker.rb b/app/workers/stuck_ci_jobs_worker.rb index ae8c980c9e4..8b0cfcc8af8 100644 --- a/app/workers/stuck_ci_jobs_worker.rb +++ b/app/workers/stuck_ci_jobs_worker.rb @@ -45,7 +45,7 @@ class StuckCiJobsWorker def search(status, timeout) builds = Ci::Build.where(status: status).where('ci_builds.updated_at < ?', timeout.ago) - builds.joins(:project).includes(:tags, :runner, project: :namespace).find_each(batch_size: 50).each do |build| + builds.joins(:project).merge(Project.without_deleted).includes(:tags, :runner, project: :namespace).find_each(batch_size: 50).each do |build| yield(build) end end diff --git a/changelogs/unreleased/30708-stop-using-deleted-at-to-filter-namespaces.yml b/changelogs/unreleased/30708-stop-using-deleted-at-to-filter-namespaces.yml new file mode 100644 index 00000000000..83ce3fb4d0a --- /dev/null +++ b/changelogs/unreleased/30708-stop-using-deleted-at-to-filter-namespaces.yml @@ -0,0 +1,4 @@ +--- +title: Removes deleted_at and pending_delete occurrences in Project related queries +merge_request: 12091 +author: diff --git a/changelogs/unreleased/33082-use-update_pipeline_schedule-for-edit-and-take_ownership-in-pipelineschedulescontroller.yml b/changelogs/unreleased/33082-use-update_pipeline_schedule-for-edit-and-take_ownership-in-pipelineschedulescontroller.yml new file mode 100644 index 00000000000..d3172c405c3 --- /dev/null +++ b/changelogs/unreleased/33082-use-update_pipeline_schedule-for-edit-and-take_ownership-in-pipelineschedulescontroller.yml @@ -0,0 +1,4 @@ +--- +title: Use authorize_update_pipeline_schedule in PipelineSchedulesController +merge_request: 11846 +author: diff --git a/changelogs/unreleased/add-group-members-counting-and-plan-related-data-on-namespaces-api.yml b/changelogs/unreleased/add-group-members-counting-and-plan-related-data-on-namespaces-api.yml new file mode 100644 index 00000000000..f2591042e98 --- /dev/null +++ b/changelogs/unreleased/add-group-members-counting-and-plan-related-data-on-namespaces-api.yml @@ -0,0 +1,4 @@ +--- +title: Add group members counting and plan related data on namespaces API +merge_request: +author: diff --git a/changelogs/unreleased/bvl-rename-all-reserved-paths.yml b/changelogs/unreleased/bvl-rename-all-reserved-paths.yml new file mode 100644 index 00000000000..f37f2fa94ae --- /dev/null +++ b/changelogs/unreleased/bvl-rename-all-reserved-paths.yml @@ -0,0 +1,4 @@ +--- +title: Rename all reserved paths that could have been created +merge_request: 11713 +author: diff --git a/changelogs/unreleased/dm-drop-default-scope-on-sortable-finders.yml b/changelogs/unreleased/dm-drop-default-scope-on-sortable-finders.yml new file mode 100644 index 00000000000..b359a25053a --- /dev/null +++ b/changelogs/unreleased/dm-drop-default-scope-on-sortable-finders.yml @@ -0,0 +1,4 @@ +--- +title: Improve performance of lookups of issues, merge requests etc by dropping unnecessary ORDER BY clause +merge_request: +author: diff --git a/changelogs/unreleased/dm-page-image-size.yml b/changelogs/unreleased/dm-page-image-size.yml new file mode 100644 index 00000000000..b18c00470fc --- /dev/null +++ b/changelogs/unreleased/dm-page-image-size.yml @@ -0,0 +1,4 @@ +--- +title: Limit OpenGraph image size to 64x64 +merge_request: +author: diff --git a/changelogs/unreleased/dm-relative-submodule-url-trailing-whitespace.yml b/changelogs/unreleased/dm-relative-submodule-url-trailing-whitespace.yml new file mode 100644 index 00000000000..616241dd941 --- /dev/null +++ b/changelogs/unreleased/dm-relative-submodule-url-trailing-whitespace.yml @@ -0,0 +1,4 @@ +--- +title: Strip trailing whitespace in relative submodule URL +merge_request: +author: diff --git a/changelogs/unreleased/fix-34417.yml b/changelogs/unreleased/fix-34417.yml new file mode 100644 index 00000000000..5f012ad0c81 --- /dev/null +++ b/changelogs/unreleased/fix-34417.yml @@ -0,0 +1,4 @@ +--- +title: Perform housekeeping only when an import of a fresh project is completed +merge_request: +author: diff --git a/changelogs/unreleased/highest-return-on-diff-investment.yml b/changelogs/unreleased/highest-return-on-diff-investment.yml new file mode 100644 index 00000000000..c8be1e0ff8f --- /dev/null +++ b/changelogs/unreleased/highest-return-on-diff-investment.yml @@ -0,0 +1,4 @@ +--- +title: Bring back branches badge to main project page +merge_request: 12548 +author: diff --git a/config/initializers/5_backend.rb b/config/initializers/5_backend.rb index 2bd159ca7f1..482613dacc9 100644 --- a/config/initializers/5_backend.rb +++ b/config/initializers/5_backend.rb @@ -1,6 +1,8 @@ -required_version = Gitlab::VersionInfo.parse(Gitlab::Shell.version_required) -current_version = Gitlab::VersionInfo.parse(Gitlab::Shell.new.version) +unless Rails.env.test? + required_version = Gitlab::VersionInfo.parse(Gitlab::Shell.version_required) + current_version = Gitlab::VersionInfo.parse(Gitlab::Shell.new.version) -unless current_version.valid? && required_version <= current_version - warn "WARNING: This version of GitLab depends on gitlab-shell #{required_version}, but you're running #{current_version}. Please update gitlab-shell." + unless current_version.valid? && required_version <= current_version + warn "WARNING: This version of GitLab depends on gitlab-shell #{required_version}, but you're running #{current_version}. Please update gitlab-shell." + end end diff --git a/config/routes/project.rb b/config/routes/project.rb index 19e18c733b1..0d0a8dff25e 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -87,13 +87,8 @@ constraints(ProjectUrlConstrainer.new) do resources :forks, only: [:index, :new, :create] resource :import, only: [:new, :create, :show] - resources :merge_requests, concerns: :awardable, constraints: { id: /\d+/ } do + resources :merge_requests, concerns: :awardable, except: [:new, :create], constraints: { id: /\d+/ } do member do - get :commits - get :diffs - get :conflicts - get :conflict_for_path - get :pipelines get :commit_change_content post :merge post :cancel_merge_when_pipeline_succeeds @@ -101,18 +96,32 @@ constraints(ProjectUrlConstrainer.new) do get :ci_environments_status post :toggle_subscription post :remove_wip - get :diff_for_path - post :resolve_conflicts post :assign_related_issues + + scope constraints: { format: nil }, action: :show do + get :commits, defaults: { tab: 'commits' } + get :pipelines, defaults: { tab: 'pipelines' } + get :diffs, defaults: { tab: 'diffs' } + end + + scope constraints: { format: 'json' }, as: :json do + get :commits + get :pipelines + get :diffs, to: 'merge_requests/diffs#show' + end + + get :diff_for_path, controller: 'merge_requests/diffs' + + scope controller: 'merge_requests/conflicts' do + get :conflicts, action: :show + get :conflict_for_path + post :resolve_conflicts + end end collection do - get :branch_from - get :branch_to - get :update_branches get :diff_for_path post :bulk_update - get :new_diffs, path: 'new/diffs' end resources :discussions, only: [], constraints: { id: /\h{40}/ } do @@ -123,6 +132,29 @@ constraints(ProjectUrlConstrainer.new) do end end + controller 'merge_requests/creations', path: 'merge_requests' do + post '', action: :create, as: nil + + scope path: 'new', as: :new_merge_request do + get '', action: :new + + scope constraints: { format: nil }, action: :new do + get :diffs, defaults: { tab: 'diffs' } + get :pipelines, defaults: { tab: 'pipelines' } + end + + scope constraints: { format: 'json' }, as: :json do + get :diffs + get :pipelines + end + + get :diff_for_path + get :update_branches + get :branch_from + get :branch_to + end + end + resources :variables, only: [:index, :show, :update, :create, :destroy] resources :triggers, only: [:index, :create, :edit, :update, :destroy] do member do diff --git a/db/post_migrate/20170525140254_rename_all_reserved_paths_again.rb b/db/post_migrate/20170525140254_rename_all_reserved_paths_again.rb new file mode 100644 index 00000000000..9441b236c8d --- /dev/null +++ b/db/post_migrate/20170525140254_rename_all_reserved_paths_again.rb @@ -0,0 +1,113 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RenameAllReservedPathsAgain < ActiveRecord::Migration + include Gitlab::Database::RenameReservedPathsMigration::V1 + + DOWNTIME = false + + disable_ddl_transaction! + + TOP_LEVEL_ROUTES = %w[ + - + .well-known + abuse_reports + admin + all + api + assets + autocomplete + ci + dashboard + explore + files + groups + health_check + help + hooks + import + invites + issues + jwt + koding + member + merge_requests + new + notes + notification_settings + oauth + profile + projects + public + repository + robots.txt + s + search + sent_notifications + services + snippets + teams + u + unicorn_test + unsubscribes + uploads + users + ].freeze + + PROJECT_WILDCARD_ROUTES = %w[ + badges + blame + blob + builds + commits + create + create_dir + edit + environments/folders + files + find_file + gitlab-lfs/objects + info/lfs/objects + new + preview + raw + refs + tree + update + wikis + ].freeze + + GROUP_ROUTES = %w[ + activity + analytics + audit_events + avatar + edit + group_members + hooks + issues + labels + ldap + ldap_group_links + merge_requests + milestones + notification_setting + pipeline_quota + projects + subgroups + ].freeze + + def up + disable_statement_timeout + + TOP_LEVEL_ROUTES.each { |route| rename_root_paths(route) } + PROJECT_WILDCARD_ROUTES.each { |route| rename_wildcard_paths(route) } + GROUP_ROUTES.each { |route| rename_child_paths(route) } + end + + def down + disable_statement_timeout + + revert_renames + end +end diff --git a/doc/api/namespaces.md b/doc/api/namespaces.md index 4ad6071a0ed..8133251dffe 100644 --- a/doc/api/namespaces.md +++ b/doc/api/namespaces.md @@ -29,22 +29,30 @@ Example response: { "id": 1, "path": "user1", - "kind": "user" + "kind": "user", + "full_path": "user1" }, { "id": 2, "path": "group1", - "kind": "group" + "kind": "group", + "full_path": "group1", + "parent_id": "null", + "members_count_with_descendants": 2 }, { "id": 3, "path": "bar", "kind": "group", "full_path": "foo/bar", + "parent_id": "9", + "members_count_with_descendants": 5 } ] ``` +**Note**: `members_count_with_descendants` are presented only for group masters/owners. + ## Search for namespace Get all namespaces that match a string in their name or path. @@ -72,6 +80,8 @@ Example response: "path": "twitter", "kind": "group", "full_path": "twitter", + "parent_id": "null", + "members_count_with_descendants": 2 } ] ``` diff --git a/doc/development/policies.md b/doc/development/policies.md new file mode 100644 index 00000000000..62141356f59 --- /dev/null +++ b/doc/development/policies.md @@ -0,0 +1,116 @@ +# `DeclarativePolicy` framework + +The DeclarativePolicy framework is designed to assist in performance of policy checks, and to enable ease of extension for EE. The DSL code in `app/policies` is what `Ability.allowed?` uses to check whether a particular action is allowed on a subject. + +The policy used is based on the subject's class name - so `Ability.allowed?(user, :some_ability, project)` will create a `ProjectPolicy` and check permissions on that. + +## Managing Permission Rules + +Permissions are broken into two parts: `conditions` and `rules`. Conditions are boolean expressions that can access the database and the environment, while rules are statically configured combinations of expressions and other rules that enable or prevent certain abilities. For an ability to be allowed, it must be enabled by at least one rule, and not prevented by any. + + +### Conditions + +Conditions are defined by the `condition` method, and are given a name and a block. The block will be executed in the context of the policy object - so it can access `@user` and `@subject`, as well as call any methods defined on the policy. Note that `@user` may be nil (in the anonymous case), but `@subject` is guaranteed to be a real instance of the subject class. + +``` ruby +class FooPolicy < BasePolicy + condition(:is_public) do + # @subject guaranteed to be an instance of Foo + @subject.public? + end + + # instance methods can be called from the condition as well + condition(:thing) { check_thing } + + def check_thing + # ... + end +end +``` + +When you define a condition, a predicate method is defined on the policy to check whether that condition passes - so in the above example, an instance of `FooPolicy` will also respond to `#is_public?` and `#thing?`. + +Conditions are cached according to their scope. Scope and ordering will be covered later. + +### Rules + +A `rule` is a logical combination of conditions and other rules, that are configured to enable or prevent certain abilities. It is important to note that the rule configuration is static - a rule's logic cannot touch the database or know about `@user` or `@subject`. This allows us to cache only at the condition level. Rules are specified through the `rule` method, which takes a block of DSL configuration, and returns an object that responds to `#enable` or `#prevent`: + +``` ruby +class FooPolicy < BasePolicy + # ... + + rule { is_public }.enable :read + rule { thing }.prevent :read + + # equivalently, + rule { is_public }.policy do + enable :read + end + + rule { ~thing }.policy do + prevent :read + end +end +``` + +Within the rule DSL, you can use: + +* A regular word mentions a condition by name - a rule that is in effect when that condition is truthy. +* `~` indicates negation +* `&` and `|` are logical combinations, also available as `all?(...)` and `any?(...)` +* `can?(:other_ability)` delegates to the rules that apply to `:other_ability`. Note that this is distinct from the instance method `can?`, which can check dynamically - this only configures a delegation to another ability. + +## Scores, Order, Performance + +To see how the rules get evaluated into a judgment, it is useful in a console to use `policy.debug(:some_ability)`. This will print the rules in the order they are evaluated. + +When a policy is asked whether a particular ability is allowed (`policy.allowed?(:some_ability)`), it does not necessarily have to compute all the conditions on the policy. First, only the rules relevant to that particular ability are selected. Then, the execution model takes advantage of short-circuiting, and attempts to sort rules based on a heuristic of how expensive they will be to calculate. The sorting is dynamic and cache-aware, so that previously calculated conditions will be considered first, before computing other conditions. + +## Scope + +Sometimes, a condition will only use data from `@user` or only from `@subject`. In this case, we want to change the scope of the caching, so that we don't recalculate conditions unnecessarily. For example, given: + +``` ruby +class FooPolicy < BasePolicy + condition(:expensive_condition) { @subject.expensive_query? } + + rule { expensive_condition }.enable :some_ability +end +``` + +Naively, if we call `Ability.can?(user1, :some_ability, foo)` and `Ability.can?(user2, :some_ability, foo)`, we would have to calculate the condition twice - since they are for different users. But if we use the `scope: :subject` option: + +``` ruby + condition(:expensive_condition, scope: :subject) { @subject.expensive_query? } +``` + +then the result of the condition will be cached globally only based on the subject - so it will not be calculated repeatedly for different users. Similarly, `scope: :user` will cache only based on the user. + +**DANGER**: If you use a `:scope` option when the condition actually uses data from +both user and subject (including a simple anonymous check!) your result will be cached at too global of a scope and will result in cache bugs. + +Sometimes we are checking permissions for a lot of users for one subject, or a lot of subjects for one user. In this case, we want to set a *preferred scope* - i.e. tell the system that we prefer rules that can be cached on the repeated parameter. For example, in `Ability.users_that_can_read_project`: + +``` ruby +def users_that_can_read_project(users, project) + DeclarativePolicy.subject_scope do + users.select { |u| allowed?(u, :read_project, project) } + end +end +``` + +This will, for example, prefer checking `project.public?` to checking `user.admin?`. + +## Delegation + +Delegation is the inclusion of rules from another policy, on a different subject. For example, + +``` ruby +class FooPolicy < BasePolicy + delegate { @subject.project } +end +``` + +will include all rules from `ProjectPolicy`. The delegated conditions will be evaluated with the correct delegated subject, and will be sorted along with the regular rules in the policy. Note that only the relevant rules for a particular ability will actually be considered. diff --git a/doc/user/profile/personal_access_tokens.md b/doc/user/profile/personal_access_tokens.md index 9488ce1ef30..f28c034e74c 100644 --- a/doc/user/profile/personal_access_tokens.md +++ b/doc/user/profile/personal_access_tokens.md @@ -14,6 +14,9 @@ accepted method of authentication when you have Once you have your token, [pass it to the API][usage] using either the `private_token` parameter or the `PRIVATE-TOKEN` header. +The expiration of personal access tokens happens on the date you define, +at midnight UTC. + ## Creating a personal access token You can create as many personal access tokens as you like from your GitLab diff --git a/features/steps/dashboard/dashboard.rb b/features/steps/dashboard/dashboard.rb index 71c69a4fdea..3c06b188f3e 100644 --- a/features/steps/dashboard/dashboard.rb +++ b/features/steps/dashboard/dashboard.rb @@ -27,7 +27,7 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps step 'I see prefilled new Merge Request page' do expect(page).to have_selector('.merge-request-form') - expect(current_path).to eq new_namespace_project_merge_request_path(@project.namespace, @project) + expect(current_path).to eq namespace_project_new_merge_request_path(@project.namespace, @project) expect(find("#merge_request_target_project_id").value).to eq @project.id.to_s expect(find("input#merge_request_source_branch").value).to eq "fix" expect(find("input#merge_request_target_branch").value).to eq "master" diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb index 9ed4f8ea1f9..02434319a08 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -266,12 +266,12 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I am redirected to the new merge request page' do - expect(current_path).to eq(new_namespace_project_merge_request_path(@project.namespace, @project)) + expect(current_path).to eq(namespace_project_new_merge_request_path(@project.namespace, @project)) end step "I am redirected to the fork's new merge request page" do fork = @user.fork_of(@project) - expect(current_path).to eq(new_namespace_project_merge_request_path(fork.namespace, fork)) + expect(current_path).to eq(namespace_project_new_merge_request_path(fork.namespace, fork)) end step 'I am redirected to the root directory' do diff --git a/lib/api/entities.rb b/lib/api/entities.rb index aa91451c9f4..cef5a0abe12 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -444,7 +444,15 @@ module API end class Namespace < Grape::Entity - expose :id, :name, :path, :kind, :full_path + expose :id, :name, :path, :kind, :full_path, :parent_id + + expose :members_count_with_descendants, if: -> (namespace, opts) { expose_members_count_with_descendants?(namespace, opts) } do |namespace, _| + namespace.users_with_descendants.count + end + + def expose_members_count_with_descendants?(namespace, opts) + namespace.kind == 'group' && Ability.allowed?(opts[:current_user], :admin_group, namespace) + end end class MemberAccess < Grape::Entity diff --git a/lib/api/helpers/runner.rb b/lib/api/helpers/runner.rb index 1369b021ea4..f8645e364ce 100644 --- a/lib/api/helpers/runner.rb +++ b/lib/api/helpers/runner.rb @@ -46,7 +46,8 @@ module API yield if block_given? - forbidden!('Project has been deleted!') unless job.project + project = job.project + forbidden!('Project has been deleted!') if project.nil? || project.pending_delete? forbidden!('Job has been erased!') if job.erased? end diff --git a/lib/api/namespaces.rb b/lib/api/namespaces.rb index 30761cb9b55..f1eaff6b0eb 100644 --- a/lib/api/namespaces.rb +++ b/lib/api/namespaces.rb @@ -17,7 +17,7 @@ module API namespaces = namespaces.search(params[:search]) if params[:search].present? - present paginate(namespaces), with: Entities::Namespace + present paginate(namespaces), with: Entities::Namespace, current_user: current_user end end end diff --git a/lib/api/projects.rb b/lib/api/projects.rb index c5df45b7902..886e97a2638 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -1,3 +1,5 @@ +require 'declarative_policy' + module API # Projects API class Projects < Grape::API @@ -396,7 +398,7 @@ module API use :pagination end get ':id/users' do - users = user_project.team.users + users = DeclarativePolicy.subject_scope { user_project.team.users } users = users.search(params[:search]) if params[:search].present? present paginate(users), with: Entities::UserBasic diff --git a/lib/ci/api/helpers.rb b/lib/ci/api/helpers.rb index 5109dc9670f..a40b6ab6c9f 100644 --- a/lib/ci/api/helpers.rb +++ b/lib/ci/api/helpers.rb @@ -28,7 +28,8 @@ module Ci yield if block_given? - forbidden!('Project has been deleted!') unless build.project + project = build.project + forbidden!('Project has been deleted!') if project.nil? || project.pending_delete? forbidden!('Build has been erased!') if build.erased? end diff --git a/lib/declarative_policy.rb b/lib/declarative_policy.rb new file mode 100644 index 00000000000..d9959bc1aff --- /dev/null +++ b/lib/declarative_policy.rb @@ -0,0 +1,58 @@ +require_dependency 'declarative_policy/cache' +require_dependency 'declarative_policy/condition' +require_dependency 'declarative_policy/dsl' +require_dependency 'declarative_policy/preferred_scope' +require_dependency 'declarative_policy/rule' +require_dependency 'declarative_policy/runner' +require_dependency 'declarative_policy/step' + +require_dependency 'declarative_policy/base' + +module DeclarativePolicy + class << self + def policy_for(user, subject, opts = {}) + cache = opts[:cache] || {} + key = Cache.policy_key(user, subject) + + cache[key] ||= class_for(subject).new(user, subject, opts) + end + + def class_for(subject) + return GlobalPolicy if subject == :global + return NilPolicy if subject.nil? + + subject = find_delegate(subject) + + subject.class.ancestors.each do |klass| + next unless klass.name + + begin + policy_class = "#{klass.name}Policy".constantize + + # NOTE: the < operator here tests whether policy_class + # inherits from Base. We can't use #is_a? because that + # tests for *instances*, not *subclasses*. + return policy_class if policy_class < Base + rescue NameError + nil + end + end + + raise "no policy for #{subject.class.name}" + end + + private + + def find_delegate(subject) + seen = Set.new + + while subject.respond_to?(:declarative_policy_delegate) + raise ArgumentError, "circular delegations" if seen.include?(subject.object_id) + seen << subject.object_id + subject = subject.declarative_policy_delegate + end + + subject + end + end +end diff --git a/lib/declarative_policy/base.rb b/lib/declarative_policy/base.rb new file mode 100644 index 00000000000..df94cafb6a1 --- /dev/null +++ b/lib/declarative_policy/base.rb @@ -0,0 +1,329 @@ +module DeclarativePolicy + class Base + # A map of ability => list of rules together with :enable + # or :prevent actions. Used to look up which rules apply to + # a given ability. See Base.ability_map + class AbilityMap + attr_reader :map + def initialize(map = {}) + @map = map + end + + # This merge behavior is different than regular hashes - if both + # share a key, the values at that key are concatenated, rather than + # overridden. + def merge(other) + conflict_proc = proc { |key, my_val, other_val| my_val + other_val } + AbilityMap.new(@map.merge(other.map, &conflict_proc)) + end + + def actions(key) + @map[key] ||= [] + end + + def enable(key, rule) + actions(key) << [:enable, rule] + end + + def prevent(key, rule) + actions(key) << [:prevent, rule] + end + end + + class << self + # The `own_ability_map` vs `ability_map` distinction is used so that + # the data structure is properly inherited - with subclasses recursively + # merging their parent class. + # + # This pattern is also used for conditions, global_actions, and delegations. + def ability_map + if self == Base + own_ability_map + else + superclass.ability_map.merge(own_ability_map) + end + end + + def own_ability_map + @own_ability_map ||= AbilityMap.new + end + + # an inheritable map of conditions, by name + def conditions + if self == Base + own_conditions + else + superclass.conditions.merge(own_conditions) + end + end + + def own_conditions + @own_conditions ||= {} + end + + # a list of global actions, generated by `prevent_all`. these aren't + # stored in `ability_map` because they aren't indexed by a particular + # ability. + def global_actions + if self == Base + own_global_actions + else + superclass.global_actions + own_global_actions + end + end + + def own_global_actions + @own_global_actions ||= [] + end + + # an inheritable map of delegations, indexed by name (which may be + # autogenerated) + def delegations + if self == Base + own_delegations + else + superclass.delegations.merge(own_delegations) + end + end + + def own_delegations + @own_delegations ||= {} + end + + # all the [rule, action] pairs that apply to a particular ability. + # we combine the specific ones looked up in ability_map with the global + # ones. + def configuration_for(ability) + ability_map.actions(ability) + global_actions + end + + ### declaration methods ### + + def delegate(name = nil, &delegation_block) + if name.nil? + @delegate_name_counter ||= 0 + @delegate_name_counter += 1 + name = :"anonymous_#{@delegate_name_counter}" + end + + name = name.to_sym + + if delegation_block.nil? + delegation_block = proc { @subject.__send__(name) } + end + + own_delegations[name] = delegation_block + end + + # Declares a rule, constructed using RuleDsl, and returns + # a PolicyDsl which is used for registering the rule with + # this class. PolicyDsl will call back into Base.enable_when, + # Base.prevent_when, and Base.prevent_all_when. + def rule(&b) + rule = RuleDsl.new(self).instance_eval(&b) + PolicyDsl.new(self, rule) + end + + # A hash in which to store calls to `desc` and `with_scope`, etc. + def last_options + @last_options ||= {}.with_indifferent_access + end + + # retrieve and zero out the previously set options (used in .condition) + def last_options! + last_options.tap { @last_options = nil } + end + + # Declare a description for the following condition. Currently unused, + # but opens the potential for explaining to users why they were or were + # not able to do something. + def desc(description) + last_options[:description] = description + end + + def with_options(opts = {}) + last_options.merge!(opts) + end + + def with_scope(scope) + with_options scope: scope + end + + def with_score(score) + with_options score: score + end + + # Declares a condition. It gets stored in `own_conditions`, and generates + # a query method based on the condition's name. + def condition(name, opts = {}, &value) + name = name.to_sym + + opts = last_options!.merge(opts) + opts[:context_key] ||= self.name + + condition = Condition.new(name, opts, &value) + + self.own_conditions[name] = condition + + define_method(:"#{name}?") { condition(name).pass? } + end + + # These next three methods are mainly called from PolicyDsl, + # and are responsible for "inverting" the relationship between + # an ability and a rule. We store in `ability_map` a map of + # abilities to rules that affect them, together with a + # symbol indicating :prevent or :enable. + def enable_when(abilities, rule) + abilities.each { |a| own_ability_map.enable(a, rule) } + end + + def prevent_when(abilities, rule) + abilities.each { |a| own_ability_map.prevent(a, rule) } + end + + # we store global prevents (from `prevent_all`) separately, + # so that they can be combined into every decision made. + def prevent_all_when(rule) + own_global_actions << [:prevent, rule] + end + end + + # A policy object contains a specific user and subject on which + # to compute abilities. For this reason it's sometimes called + # "context" within the framework. + # + # It also stores a reference to the cache, so it can be used + # to cache computations by e.g. ManifestCondition. + attr_reader :user, :subject, :cache + def initialize(user, subject, opts = {}) + @user = user + @subject = subject + @cache = opts[:cache] || {} + end + + # helper for checking abilities on this and other subjects + # for the current user. + def can?(ability, new_subject = :_self) + return allowed?(ability) if new_subject == :_self + + policy_for(new_subject).allowed?(ability) + end + + # This is the main entry point for permission checks. It constructs + # or looks up a Runner for the given ability and asks it if it passes. + def allowed?(*abilities) + abilities.all? { |a| runner(a).pass? } + end + + # The inverse of #allowed?, used mainly in specs. + def disallowed?(*abilities) + abilities.all? { |a| !runner(a).pass? } + end + + # computes the given ability and prints a helpful debugging output + # showing which + def debug(ability, *a) + runner(ability).debug(*a) + end + + desc "Unknown user" + condition(:anonymous, scope: :user, score: 0) { @user.nil? } + + desc "By default" + condition(:default, scope: :global, score: 0) { true } + + def repr + subject_repr = + if @subject.respond_to?(:id) + "#{@subject.class.name}/#{@subject.id}" + else + @subject.inspect + end + + user_repr = + if @user + @user.to_reference + else + "<anonymous>" + end + + "(#{user_repr} : #{subject_repr})" + end + + def inspect + "#<#{self.class.name} #{repr}>" + end + + # returns a Runner for the given ability, capable of computing whether + # the ability is allowed. Runners are cached on the policy (which itself + # is cached on @cache), and caches its result. This is how we perform caching + # at the ability level. + def runner(ability) + ability = ability.to_sym + @runners ||= {} + @runners[ability] ||= + begin + delegated_runners = delegated_policies.values.compact.map { |p| p.runner(ability) } + own_runner = Runner.new(own_steps(ability)) + delegated_runners.inject(own_runner, &:merge_runner) + end + end + + # Helpers for caching. Used by ManifestCondition in performing condition + # computation. + # + # NOTE we can't use ||= here because the value might be the + # boolean `false` + def cache(key, &b) + return @cache[key] if cached?(key) + @cache[key] = yield + end + + def cached?(key) + !@cache[key].nil? + end + + # returns a ManifestCondition capable of computing itself. The computation + # will use our own @cache. + def condition(name) + name = name.to_sym + @_conditions ||= {} + @_conditions[name] ||= + begin + raise "invalid condition #{name}" unless self.class.conditions.key?(name) + ManifestCondition.new(self.class.conditions[name], self) + end + end + + # used in specs - returns true if there is no possible way for any action + # to be allowed, determined only by the global :prevent_all rules. + def banned? + global_steps = self.class.global_actions.map { |(action, rule)| Step.new(self, rule, action) } + !Runner.new(global_steps).pass? + end + + # A list of other policies that we've delegated to (see `Base.delegate`) + def delegated_policies + @delegated_policies ||= self.class.delegations.transform_values do |block| + new_subject = instance_eval(&block) + + # never delegate to nil, as that would immediately prevent_all + next if new_subject.nil? + + policy_for(new_subject) + end + end + + def policy_for(other_subject) + DeclarativePolicy.policy_for(@user, other_subject, cache: @cache) + end + + protected + + # constructs steps that come from this policy and not from any delegations + def own_steps(ability) + rules = self.class.configuration_for(ability) + rules.map { |(action, rule)| Step.new(self, rule, action) } + end + end +end diff --git a/lib/declarative_policy/cache.rb b/lib/declarative_policy/cache.rb new file mode 100644 index 00000000000..b8cc60074c7 --- /dev/null +++ b/lib/declarative_policy/cache.rb @@ -0,0 +1,32 @@ +module DeclarativePolicy + module Cache + class << self + def user_key(user) + return '<anonymous>' if user.nil? + id_for(user) + end + + def policy_key(user, subject) + u = user_key(user) + s = subject_key(subject) + "/dp/policy/#{u}/#{s}" + end + + def subject_key(subject) + return '<nil>' if subject.nil? + return subject.inspect if subject.is_a?(Symbol) + "#{subject.class.name}:#{id_for(subject)}" + end + + private + + def id_for(obj) + if obj.respond_to?(:id) && obj.id + obj.id.to_s + else + "##{obj.object_id}" + end + end + end + end +end diff --git a/lib/declarative_policy/condition.rb b/lib/declarative_policy/condition.rb new file mode 100644 index 00000000000..9d7cf6b9726 --- /dev/null +++ b/lib/declarative_policy/condition.rb @@ -0,0 +1,102 @@ +module DeclarativePolicy + # A Condition is the data structure that is created by the + # `condition` declaration on DeclarativePolicy::Base. It is + # more or less just a struct of the data passed to that + # declaration. It holds on to the block to be instance_eval'd + # on a context (instance of Base) later, via #compute. + class Condition + attr_reader :name, :description, :scope + attr_reader :manual_score + attr_reader :context_key + def initialize(name, opts = {}, &compute) + @name = name + @compute = compute + @scope = opts.fetch(:scope, :normal) + @description = opts.delete(:description) + @context_key = opts[:context_key] + @manual_score = opts.fetch(:score, nil) + end + + def compute(context) + !!context.instance_eval(&@compute) + end + + def key + "#{@context_key}/#{@name}" + end + end + + # In contrast to a Condition, a ManifestCondition contains + # a Condition and a context object, and is capable of calculating + # a result itself. This is the return value of Base#condition. + class ManifestCondition + def initialize(condition, context) + @condition = condition + @context = context + end + + # The main entry point - does this condition pass? We reach into + # the context's cache here so that we can share in the global + # cache (often RequestStore or similar). + def pass? + @context.cache(cache_key) { @condition.compute(@context) } + end + + # Whether we've already computed this condition. + def cached? + @context.cached?(cache_key) + end + + # This is used to score Rule::Condition. See Rule::Condition#score + # and Runner#steps_by_score for how scores are used. + # + # The number here is intended to represent, abstractly, how + # expensive it would be to calculate this condition. + # + # See #cache_key for info about @condition.scope. + def score + # If we've been cached, no computation is necessary. + return 0 if cached? + + # Use the override from condition(score: ...) if present + return @condition.manual_score if @condition.manual_score + + # Global scope rules are cheap due to max cache sharing + return 2 if @condition.scope == :global + + # "Normal" rules can't share caches with any other policies + return 16 if @condition.scope == :normal + + # otherwise, we're :user or :subject scope, so it's 4 if + # the caller has declared a preference + return 4 if @condition.scope == DeclarativePolicy.preferred_scope + + # and 8 for all other :user or :subject scope conditions. + 8 + end + + private + + # This method controls the caching for the condition. This is where + # the condition(scope: ...) option comes into play. Notice that + # depending on the scope, we may cache only by the user or only by + # the subject, resulting in sharing across different policy objects. + def cache_key + case @condition.scope + when :normal then "/dp/condition/#{@condition.key}/#{user_key},#{subject_key}" + when :user then "/dp/condition/#{@condition.key}/#{user_key}" + when :subject then "/dp/condition/#{@condition.key}/#{subject_key}" + when :global then "/dp/condition/#{@condition.key}" + else raise 'invalid scope' + end + end + + def user_key + Cache.user_key(@context.user) + end + + def subject_key + Cache.subject_key(@context.subject) + end + end +end diff --git a/lib/declarative_policy/dsl.rb b/lib/declarative_policy/dsl.rb new file mode 100644 index 00000000000..b26807a7622 --- /dev/null +++ b/lib/declarative_policy/dsl.rb @@ -0,0 +1,103 @@ +module DeclarativePolicy + # The DSL evaluation context inside rule { ... } blocks. + # Responsible for creating and combining Rule objects. + # + # See Base.rule + class RuleDsl + def initialize(context_class) + @context_class = context_class + end + + def can?(ability) + Rule::Ability.new(ability) + end + + def all?(*rules) + Rule::And.make(rules) + end + + def any?(*rules) + Rule::Or.make(rules) + end + + def none?(*rules) + ~Rule::Or.new(rules) + end + + def cond(condition) + Rule::Condition.new(condition) + end + + def delegate(delegate_name, condition) + Rule::DelegatedCondition.new(delegate_name, condition) + end + + def method_missing(m, *a, &b) + return super unless a.size == 0 && !block_given? + + if @context_class.delegations.key?(m) + DelegateDsl.new(self, m) + else + cond(m.to_sym) + end + end + end + + # Used when the name of a delegate is mentioned in + # the rule DSL. + class DelegateDsl + def initialize(rule_dsl, delegate_name) + @rule_dsl = rule_dsl + @delegate_name = delegate_name + end + + def method_missing(m, *a, &b) + return super unless a.size == 0 && !block_given? + + @rule_dsl.delegate(@delegate_name, m) + end + end + + # The return value of a rule { ... } declaration. + # Can call back to register rules with the containing + # Policy class (context_class here). See Base.rule + # + # Note that the #policy method just performs an #instance_eval, + # which is useful for multiple #enable or #prevent callse. + # + # Also provides a #method_missing proxy to the context + # class's class methods, so that helper methods can be + # defined and used in a #policy { ... } block. + class PolicyDsl + def initialize(context_class, rule) + @context_class = context_class + @rule = rule + end + + def policy(&b) + instance_eval(&b) + end + + def enable(*abilities) + @context_class.enable_when(abilities, @rule) + end + + def prevent(*abilities) + @context_class.prevent_when(abilities, @rule) + end + + def prevent_all + @context_class.prevent_all_when(@rule) + end + + def method_missing(m, *a, &b) + return super unless @context_class.respond_to?(m) + + @context_class.__send__(m, *a, &b) + end + + def respond_to_missing?(m) + @context_class.respond_to?(m) || super + end + end +end diff --git a/lib/declarative_policy/preferred_scope.rb b/lib/declarative_policy/preferred_scope.rb new file mode 100644 index 00000000000..b0754098149 --- /dev/null +++ b/lib/declarative_policy/preferred_scope.rb @@ -0,0 +1,28 @@ +module DeclarativePolicy + PREFERRED_SCOPE_KEY = :"DeclarativePolicy.preferred_scope" + + class << self + def with_preferred_scope(scope, &b) + Thread.current[PREFERRED_SCOPE_KEY], old_scope = scope, Thread.current[PREFERRED_SCOPE_KEY] + yield + ensure + Thread.current[PREFERRED_SCOPE_KEY] = old_scope + end + + def preferred_scope + Thread.current[PREFERRED_SCOPE_KEY] + end + + def user_scope(&b) + with_preferred_scope(:user, &b) + end + + def subject_scope(&b) + with_preferred_scope(:subject, &b) + end + + def preferred_scope=(scope) + Thread.current[PREFERRED_SCOPE_KEY] = scope + end + end +end diff --git a/lib/declarative_policy/rule.rb b/lib/declarative_policy/rule.rb new file mode 100644 index 00000000000..bfcec241489 --- /dev/null +++ b/lib/declarative_policy/rule.rb @@ -0,0 +1,301 @@ +module DeclarativePolicy + module Rule + # A Rule is the object that results from the `rule` declaration, + # usually built using the DSL in `RuleDsl`. It is a basic logical + # combination of building blocks, and is capable of deciding, + # given a context (instance of DeclarativePolicy::Base) whether it + # passes or not. Note that this decision doesn't by itself know + # how that affects the actual ability decision - for that, a + # `Step` is used. + class Base + def self.make(*a) + new(*a).simplify + end + + # true or false whether this rule passes. + # `context` is a policy - an instance of + # DeclarativePolicy::Base. + def pass?(context) + raise 'abstract' + end + + # same as #pass? except refuses to do any I/O, + # returning nil if the result is not yet cached. + # used for accurately scoring And/Or + def cached_pass?(context) + raise 'abstract' + end + + # abstractly, how long would it take to compute + # this rule? lower-scored rules are tried first. + def score(context) + raise 'abstract' + end + + # unwrap double negatives and nested and/or + def simplify + self + end + + # convenience combination methods + def or(other) + Or.make([self, other]) + end + + def and(other) + And.make([self, other]) + end + + def negate + Not.make(self) + end + + alias_method :|, :or + alias_method :&, :and + alias_method :~@, :negate + + def inspect + "#<Rule #{repr}>" + end + end + + # A rule that checks a condition. This is the + # type of rule that results from a basic bareword + # in the rule dsl (see RuleDsl#method_missing). + class Condition < Base + def initialize(name) + @name = name + end + + # we delegate scoring to the condition. See + # ManifestCondition#score. + def score(context) + context.condition(@name).score + end + + # Let the ManifestCondition from the context + # decide whether we pass. + def pass?(context) + context.condition(@name).pass? + end + + # returns nil unless it's already cached + def cached_pass?(context) + condition = context.condition(@name) + return nil unless condition.cached? + condition.pass? + end + + def description(context) + context.class.conditions[@name].description + end + + def repr + @name.to_s + end + end + + # A rule constructed from DelegateDsl - using a condition from a + # delegated policy. + class DelegatedCondition < Base + # Internal use only - this is rescued each time it's raised. + MissingDelegate = Class.new(StandardError) + + def initialize(delegate_name, name) + @delegate_name = delegate_name + @name = name + end + + def delegated_context(context) + policy = context.delegated_policies[@delegate_name] + raise MissingDelegate if policy.nil? + policy + end + + def score(context) + delegated_context(context).condition(@name).score + rescue MissingDelegate + 0 + end + + def cached_pass?(context) + condition = delegated_context(context).condition(@name) + return nil unless condition.cached? + condition.pass? + rescue MissingDelegate + false + end + + def pass?(context) + delegated_context(context).condition(@name).pass? + rescue MissingDelegate + false + end + + def repr + "#{@delegate_name}.#{@name}" + end + end + + # A rule constructed from RuleDsl#can?. Computes a different ability + # on the same subject. + class Ability < Base + attr_reader :ability + def initialize(ability) + @ability = ability + end + + # We ask the ability's runner for a score + def score(context) + context.runner(@ability).score + end + + def pass?(context) + context.allowed?(@ability) + end + + def cached_pass?(context) + runner = context.runner(@ability) + return nil unless runner.cached? + runner.pass? + end + + def description(context) + "User can #{@ability.inspect}" + end + + def repr + "can?(#{@ability.inspect})" + end + end + + # Logical `and`, containing a list of rules. Only passes + # if all of them do. + class And < Base + attr_reader :rules + def initialize(rules) + @rules = rules + end + + def simplify + simplified_rules = @rules.flat_map do |rule| + simplified = rule.simplify + case simplified + when And then simplified.rules + else [simplified] + end + end + + And.new(simplified_rules) + end + + def score(context) + return 0 unless cached_pass?(context).nil? + + # note that cached rules will have score 0 anyways. + @rules.map { |r| r.score(context) }.inject(0, :+) + end + + def pass?(context) + # try to find a cached answer before + # checking in order + cached = cached_pass?(context) + return cached unless cached.nil? + + @rules.all? { |r| r.pass?(context) } + end + + def cached_pass?(context) + passes = @rules.map { |r| r.cached_pass?(context) } + return false if passes.any? { |p| p == false } + return true if passes.all? { |p| p == true } + + nil + end + + def repr + "all?(#{rules.map(&:repr).join(', ')})" + end + end + + # Logical `or`. Mirrors And. + class Or < Base + attr_reader :rules + def initialize(rules) + @rules = rules + end + + def pass?(context) + cached = cached_pass?(context) + return cached unless cached.nil? + + @rules.any? { |r| r.pass?(context) } + end + + def simplify + simplified_rules = @rules.flat_map do |rule| + simplified = rule.simplify + case simplified + when Or then simplified.rules + else [simplified] + end + end + + Or.new(simplified_rules) + end + + def cached_pass?(context) + passes = @rules.map { |r| r.cached_pass?(context) } + return true if passes.any? { |p| p == true } + return false if passes.all? { |p| p == false } + + nil + end + + def score(context) + return 0 unless cached_pass?(context).nil? + @rules.map { |r| r.score(context) }.inject(0, :+) + end + + def repr + "any?(#{@rules.map(&:repr).join(', ')})" + end + end + + class Not < Base + attr_reader :rule + def initialize(rule) + @rule = rule + end + + def simplify + case @rule + when And then Or.new(@rule.rules.map(&:negate)).simplify + when Or then And.new(@rule.rules.map(&:negate)).simplify + when Not then @rule.rule.simplify + else Not.new(@rule.simplify) + end + end + + def pass?(context) + !@rule.pass?(context) + end + + def cached_pass?(context) + case @rule.cached_pass?(context) + when nil then nil + when true then false + when false then true + end + end + + def score(context) + @rule.score(context) + end + + def repr + "~#{@rule.repr}" + end + end + end +end diff --git a/lib/declarative_policy/runner.rb b/lib/declarative_policy/runner.rb new file mode 100644 index 00000000000..b5c615da4e3 --- /dev/null +++ b/lib/declarative_policy/runner.rb @@ -0,0 +1,181 @@ +module DeclarativePolicy + class Runner + class State + def initialize + @enabled = false + @prevented = false + end + + def enable! + @enabled = true + end + + def enabled? + @enabled + end + + def prevent! + @prevented = true + end + + def prevented? + @prevented + end + + def pass? + !prevented? && enabled? + end + end + + # a Runner contains a list of Steps to be run. + attr_reader :steps + def initialize(steps) + @steps = steps + end + + # We make sure only to run any given Runner once, + # and just continue to use the resulting @state + # that's left behind. + def cached? + !!@state + end + + # used by Rule::Ability. See #steps_by_score + def score + return 0 if cached? + steps.map(&:score).inject(0, :+) + end + + def merge_runner(other) + Runner.new(@steps + other.steps) + end + + # The main entry point, called for making an ability decision. + # See #run and DeclarativePolicy::Base#can? + def pass? + run unless cached? + + @state.pass? + end + + # see DeclarativePolicy::Base#debug + def debug(out = $stderr) + run(out) + end + + private + + def flatten_steps! + @steps = @steps.flat_map { |s| s.flattened(@steps) } + end + + # This method implements the semantic of "one enable and no prevents". + # It relies on #steps_by_score for the main loop, and updates @state + # with the result of the step. + def run(debug = nil) + @state = State.new + + steps_by_score do |step, score| + passed = nil + case step.action + when :enable then + # we only check :enable actions if they have a chance of + # changing the outcome - if no other rule has enabled or + # prevented. + unless @state.enabled? || @state.prevented? + passed = step.pass? + @state.enable! if passed + end + + debug << inspect_step(step, score, passed) if debug + when :prevent then + # we only check :prevent actions if the state hasn't already + # been prevented. + unless @state.prevented? + passed = step.pass? + if passed + @state.prevent! + return unless debug + end + end + + debug << inspect_step(step, score, passed) if debug + else raise "invalid action #{step.action.inspect}" + end + end + + @state + end + + # This is the core spot where all those `#score` methods matter. + # It is critcal for performance to run steps in the correct order, + # so that we don't compute expensive conditions (potentially n times + # if we're called on, say, a large list of users). + # + # In order to determine the cheapest step to run next, we rely on + # Step#score, which returns a numerical rating of how expensive + # it would be to calculate - the lower the better. It would be + # easy enough to statically sort by these scores, but we can do + # a little better - the scores are cache-aware (conditions that + # are already in the cache have score 0), which means that running + # a step can actually change the scores of other steps. + # + # So! The way we sort here involves re-scoring at every step. This + # is by necessity quadratic, but most of the time the number of steps + # will be low. But just in case, if the number of steps exceeds 50, + # we print a warning and fall back to a static sort. + # + # For each step, we yield the step object along with the computed score + # for debugging purposes. + def steps_by_score(&b) + flatten_steps! + + if @steps.size > 50 + warn "DeclarativePolicy: large number of steps (#{steps.size}), falling back to static sort" + + @steps.map { |s| [s.score, s] }.sort_by { |(score, _)| score }.each do |(score, step)| + yield step, score + end + + return + end + + steps = Set.new(@steps) + + loop do + return if steps.empty? + + # if the permission hasn't yet been enabled and we only have + # prevent steps left, we short-circuit the state here + @state.prevent! if !@state.enabled? && steps.all?(&:prevent?) + + lowest_score = Float::INFINITY + next_step = nil + + steps.each do |step| + score = step.score + if score < lowest_score + next_step = step + lowest_score = score + end + end + + steps.delete(next_step) + + yield next_step, lowest_score + end + end + + # Formatter for debugging output. + def inspect_step(step, original_score, passed) + symbol = + case passed + when true then '+' + when false then '-' + when nil then ' ' + end + + "#{symbol} [#{original_score.to_i}] #{step.repr}\n" + end + end +end diff --git a/lib/declarative_policy/step.rb b/lib/declarative_policy/step.rb new file mode 100644 index 00000000000..3469fe9f991 --- /dev/null +++ b/lib/declarative_policy/step.rb @@ -0,0 +1,86 @@ +module DeclarativePolicy + # This object represents one step in the runtime decision of whether + # an ability is allowed. It contains a Rule and a context (instance + # of DeclarativePolicy::Base), which contains the user, the subject, + # and the cache. It also contains an "action", which is the symbol + # :prevent or :enable. + class Step + attr_reader :context, :rule, :action + def initialize(context, rule, action) + @context = context + @rule = rule + @action = action + end + + # In the flattening process, duplicate steps may be generated in the + # same rule. This allows us to eliminate those (see Runner#steps_by_score + # and note its use of a Set) + def ==(other) + @context == other.context && @rule == other.rule && @action == other.action + end + + # In the runner, steps are sorted dynamically by score, so that + # we are sure to compute them in close to the optimal order. + # + # See also Rule#score, ManifestCondition#score, and Runner#steps_by_score. + def score + # we slightly prefer the preventative actions + # since they are more likely to short-circuit + case @action + when :prevent + @rule.score(@context) * (7.0 / 8) + when :enable + @rule.score(@context) + end + end + + def with_action(action) + Step.new(@context, @rule, action) + end + + def enable? + @action == :enable + end + + def prevent? + @action == :prevent + end + + # This rather complex method allows us to split rules into parts so that + # they can be sorted independently for better optimization + def flattened(roots) + case @rule + when Rule::Or + # A single `Or` step is the same as each of its elements as separate steps + @rule.rules.flat_map { |r| Step.new(@context, r, @action).flattened(roots) } + when Rule::Ability + # This looks like a weird micro-optimization but it buys us quite a lot + # in some cases. If we depend on an Ability (i.e. a `can?(...)` rule), + # and that ability *only* has :enable actions (modulo some actions that + # we already have taken care of), then its rules can be safely inlined. + steps = @context.runner(@rule.ability).steps.reject { |s| roots.include?(s) } + + if steps.all?(&:enable?) + # in the case that we are a :prevent step, each inlined step becomes + # an independent :prevent, even though it was an :enable in its initial + # context. + steps.map! { |s| s.with_action(:prevent) } if prevent? + + steps.flat_map { |s| s.flattened(roots) } + else + [self] + end + else + [self] + end + end + + def pass? + @rule.pass?(@context) + end + + def repr + "#{@action} when #{@rule.repr} (#{@context.repr})" + end + end +end diff --git a/lib/gitlab/allowable.rb b/lib/gitlab/allowable.rb index e4f7cad2b79..45c2b01dd8f 100644 --- a/lib/gitlab/allowable.rb +++ b/lib/gitlab/allowable.rb @@ -1,7 +1,7 @@ module Gitlab module Allowable - def can?(user, action, subject = :global) - Ability.allowed?(user, action, subject) + def can?(*args) + Ability.allowed?(*args) end end end diff --git a/lib/gitlab/database/rename_reserved_paths_migration/v1.rb b/lib/gitlab/database/rename_reserved_paths_migration/v1.rb index 89530082cd2..f333ff22300 100644 --- a/lib/gitlab/database/rename_reserved_paths_migration/v1.rb +++ b/lib/gitlab/database/rename_reserved_paths_migration/v1.rb @@ -29,6 +29,11 @@ module Gitlab paths = Array(paths) RenameNamespaces.new(paths, self).rename_namespaces(type: :top_level) end + + def revert_renames + RenameProjects.new([], self).revert_renames + RenameNamespaces.new([], self).revert_renames + end end end end diff --git a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base.rb b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base.rb index d8163d7da11..33f8939bc61 100644 --- a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base.rb +++ b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base.rb @@ -6,7 +6,10 @@ module Gitlab attr_reader :paths, :migration delegate :update_column_in_batches, + :execute, :replace_sql, + :quote_string, + :say, to: :migration def initialize(paths, migration) @@ -26,24 +29,45 @@ module Gitlab new_path = rename_path(namespace_path, old_path) new_full_path = join_routable_path(namespace_path, new_path) + perform_rename(routable, old_full_path, new_full_path) + + [old_full_path, new_full_path] + end + + def perform_rename(routable, old_full_path, new_full_path) # skips callbacks & validations + new_path = new_full_path.split('/').last routable.class.where(id: routable) .update_all(path: new_path) rename_routes(old_full_path, new_full_path) - - [old_full_path, new_full_path] end def rename_routes(old_full_path, new_full_path) + routes = Route.arel_table + + quoted_old_full_path = quote_string(old_full_path) + quoted_old_wildcard_path = quote_string("#{old_full_path}/%") + + filter = if Database.mysql? + "lower(routes.path) = lower('#{quoted_old_full_path}') "\ + "OR routes.path LIKE '#{quoted_old_wildcard_path}'" + else + "routes.id IN "\ + "( SELECT routes.id FROM routes WHERE lower(routes.path) = lower('#{quoted_old_full_path}') "\ + "UNION SELECT routes.id FROM routes WHERE routes.path ILIKE '#{quoted_old_wildcard_path}' )" + end + replace_statement = replace_sql(Route.arel_table[:path], old_full_path, new_full_path) - update_column_in_batches(:routes, :path, replace_statement) do |table, query| - path_or_children = table[:path].matches_any([old_full_path, "#{old_full_path}/%"]) - query.where(path_or_children) - end + update = Arel::UpdateManager.new(ActiveRecord::Base) + .table(routes) + .set([[routes[:path], replace_statement]]) + .where(Arel::Nodes::SqlLiteral.new(filter)) + + execute(update.to_sql) end def rename_path(namespace_path, path_was) @@ -86,32 +110,74 @@ module Gitlab def move_folders(directory, old_relative_path, new_relative_path) old_path = File.join(directory, old_relative_path) - return unless File.directory?(old_path) + unless File.directory?(old_path) + say "#{old_path} doesn't exist, skipping" + return + end new_path = File.join(directory, new_relative_path) FileUtils.mv(old_path, new_path) end def remove_cached_html_for_projects(project_ids) - update_column_in_batches(:projects, :description_html, nil) do |table, query| - query.where(table[:id].in(project_ids)) - end - - update_column_in_batches(:issues, :description_html, nil) do |table, query| - query.where(table[:project_id].in(project_ids)) + project_ids.each do |project_id| + update_column_in_batches(:projects, :description_html, nil) do |table, query| + query.where(table[:id].eq(project_id)) + end + + update_column_in_batches(:issues, :description_html, nil) do |table, query| + query.where(table[:project_id].eq(project_id)) + end + + update_column_in_batches(:merge_requests, :description_html, nil) do |table, query| + query.where(table[:target_project_id].eq(project_id)) + end + + update_column_in_batches(:notes, :note_html, nil) do |table, query| + query.where(table[:project_id].eq(project_id)) + end + + update_column_in_batches(:milestones, :description_html, nil) do |table, query| + query.where(table[:project_id].eq(project_id)) + end end + end - update_column_in_batches(:merge_requests, :description_html, nil) do |table, query| - query.where(table[:target_project_id].in(project_ids)) + def track_rename(type, old_path, new_path) + key = redis_key_for_type(type) + Gitlab::Redis.with do |redis| + redis.lpush(key, [old_path, new_path].to_json) + redis.expire(key, 2.weeks.to_i) end + say "tracked rename: #{key}: #{old_path} -> #{new_path}" + end - update_column_in_batches(:notes, :note_html, nil) do |table, query| - query.where(table[:project_id].in(project_ids)) + def reverts_for_type(type) + key = redis_key_for_type(type) + + Gitlab::Redis.with do |redis| + failed_reverts = [] + + while rename_info = redis.lpop(key) + path_before_rename, path_after_rename = JSON.parse(rename_info) + say "renaming #{type} from #{path_after_rename} back to #{path_before_rename}" + begin + yield(path_before_rename, path_after_rename) + rescue StandardError => e + failed_reverts << rename_info + say "Renaming #{type} from #{path_after_rename} back to "\ + "#{path_before_rename} failed. Review the error and try "\ + "again by running the `down` action. \n"\ + "#{e.message}: \n #{e.backtrace.join("\n")}" + end + end + + failed_reverts.each { |rename_info| redis.lpush(key, rename_info) } end + end - update_column_in_batches(:milestones, :description_html, nil) do |table, query| - query.where(table[:project_id].in(project_ids)) - end + def redis_key_for_type(type) + "rename:#{migration.name}:#{type}" end def file_storage? diff --git a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb index da7e2cb2e85..05b86f32ce2 100644 --- a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb +++ b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb @@ -26,6 +26,12 @@ module Gitlab def rename_namespace(namespace) old_full_path, new_full_path = rename_path_for_routable(namespace) + track_rename('namespace', old_full_path, new_full_path) + + rename_namespace_dependencies(namespace, old_full_path, new_full_path) + end + + def rename_namespace_dependencies(namespace, old_full_path, new_full_path) move_repositories(namespace, old_full_path, new_full_path) move_uploads(old_full_path, new_full_path) move_pages(old_full_path, new_full_path) @@ -33,6 +39,23 @@ module Gitlab remove_cached_html_for_projects(projects_for_namespace(namespace).map(&:id)) end + def revert_renames + reverts_for_type('namespace') do |path_before_rename, current_path| + matches_path = MigrationClasses::Route.arel_table[:path].matches(current_path) + namespace = MigrationClasses::Namespace.joins(:route) + .where(matches_path).first&.becomes(MigrationClasses::Namespace) + + if namespace + perform_rename(namespace, current_path, path_before_rename) + + rename_namespace_dependencies(namespace, current_path, path_before_rename) + else + say "Couldn't rename namespace from #{current_path} back to #{path_before_rename}, "\ + "namespace was renamed, or no longer exists at the expected path" + end + end + end + def rename_user(old_username, new_username) MigrationClasses::User.where(username: old_username) .update_all(username: new_username) diff --git a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb index 448717eb744..75a75f61953 100644 --- a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb +++ b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb @@ -16,12 +16,37 @@ module Gitlab def rename_project(project) old_full_path, new_full_path = rename_path_for_routable(project) + track_rename('project', old_full_path, new_full_path) + + move_project_folders(project, old_full_path, new_full_path) + end + + def move_project_folders(project, old_full_path, new_full_path) move_repository(project, old_full_path, new_full_path) move_repository(project, "#{old_full_path}.wiki", "#{new_full_path}.wiki") move_uploads(old_full_path, new_full_path) move_pages(old_full_path, new_full_path) end + def revert_renames + reverts_for_type('project') do |path_before_rename, current_path| + matches_path = MigrationClasses::Route.arel_table[:path].matches(current_path) + project = MigrationClasses::Project.joins(:route) + .where(matches_path).first + + if project + perform_rename(project, current_path, path_before_rename) + + move_project_folders(project, current_path, path_before_rename) + else + say "Couldn't rename project from #{current_path} back to "\ + "#{path_before_rename}, project was renamed or no longer "\ + "exists at the expected path." + + end + end + end + def move_repository(project, old_path, new_path) unless gitlab_shell.mv_repository(project.repository_storage_path, old_path, diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb index 936606152e9..4175746be39 100644 --- a/lib/gitlab/git.rb +++ b/lib/gitlab/git.rb @@ -7,8 +7,10 @@ module Gitlab CommandError = Class.new(StandardError) class << self + include Gitlab::EncodingHelper + def ref_name(ref) - ref.sub(/\Arefs\/(tags|heads)\//, '') + encode! ref.sub(/\Arefs\/(tags|heads)\//, '') end def branch_name(ref) diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index dd296983491..23d0c8a9bdb 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -113,9 +113,7 @@ module Gitlab def local_branches(sort_by: nil) gitaly_migrate(:local_branches) do |is_enabled| if is_enabled - gitaly_ref_client.local_branches(sort_by: sort_by).map do |gitaly_branch| - Gitlab::Git::Branch.new(self, gitaly_branch.name, gitaly_branch) - end + gitaly_ref_client.local_branches(sort_by: sort_by) else branches(filter: :local, sort_by: sort_by) end diff --git a/lib/gitlab/gitaly_client/ref.rb b/lib/gitlab/gitaly_client/ref.rb index 6d5f54dd959..2d61992f595 100644 --- a/lib/gitlab/gitaly_client/ref.rb +++ b/lib/gitlab/gitaly_client/ref.rb @@ -1,8 +1,11 @@ module Gitlab module GitalyClient class Ref + include Gitlab::EncodingHelper + # 'repository' is a Gitlab::Git::Repository def initialize(repository) + @repository = repository @gitaly_repo = repository.gitaly_repository @storage = repository.storage end @@ -16,13 +19,13 @@ module Gitlab def branch_names request = Gitaly::FindAllBranchNamesRequest.new(repository: @gitaly_repo) response = GitalyClient.call(@storage, :ref, :find_all_branch_names, request) - consume_refs_response(response, prefix: 'refs/heads/') + consume_refs_response(response) { |name| Gitlab::Git.branch_name(name) } end def tag_names request = Gitaly::FindAllTagNamesRequest.new(repository: @gitaly_repo) response = GitalyClient.call(@storage, :ref, :find_all_tag_names, request) - consume_refs_response(response, prefix: 'refs/tags/') + consume_refs_response(response) { |name| Gitlab::Git.tag_name(name) } end def find_ref_name(commit_id, ref_prefix) @@ -51,20 +54,28 @@ module Gitlab private - def consume_refs_response(response, prefix:) - response.flat_map do |r| - r.names.map { |name| name.sub(/\A#{Regexp.escape(prefix)}/, '') } - end + def consume_refs_response(response) + response.flat_map { |message| message.names.map { |name| yield(name) } } end def sort_by_param(sort_by) + sort_by = 'name' if sort_by == 'name_asc' + enum_value = Gitaly::FindLocalBranchesRequest::SortBy.resolve(sort_by.upcase.to_sym) raise ArgumentError, "Invalid sort_by key `#{sort_by}`" unless enum_value enum_value end def consume_branches_response(response) - response.flat_map { |r| r.branches } + response.flat_map do |message| + message.branches.map do |gitaly_branch| + Gitlab::Git::Branch.new( + @repository, + encode!(gitaly_branch.name.dup), + gitaly_branch.commit_id + ) + end + end end end end diff --git a/lib/gitlab/view/presenter/base.rb b/lib/gitlab/view/presenter/base.rb index dbfe0941e4d..841fb681435 100644 --- a/lib/gitlab/view/presenter/base.rb +++ b/lib/gitlab/view/presenter/base.rb @@ -15,6 +15,11 @@ module Gitlab super(user, action, overriden_subject || subject) end + # delegate all #can? queries to the subject + def declarative_policy_delegate + subject + end + class_methods do def presenter? true diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb index c20cf6a4291..561bc219bb4 100644 --- a/spec/controllers/projects/blob_controller_spec.rb +++ b/spec/controllers/projects/blob_controller_spec.rb @@ -235,7 +235,7 @@ describe Projects::BlobController do put :update, default_params expect(response).to redirect_to( - new_namespace_project_merge_request_path( + namespace_project_new_merge_request_path( forked_project.namespace, forked_project, merge_request: { diff --git a/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb b/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb new file mode 100644 index 00000000000..9278ac8edd8 --- /dev/null +++ b/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb @@ -0,0 +1,307 @@ +require 'spec_helper' + +describe Projects::MergeRequests::ConflictsController do + let(:project) { create(:project) } + let(:user) { project.owner } + let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) } + let(:merge_request_with_conflicts) do + create(:merge_request, source_branch: 'conflict-resolvable', target_branch: 'conflict-start', source_project: project) do |mr| + mr.mark_as_unmergeable + end + end + + before do + sign_in(user) + end + + describe 'GET show' do + context 'when the conflicts cannot be resolved in the UI' do + before do + allow_any_instance_of(Gitlab::Conflict::Parser) + .to receive(:parse).and_raise(Gitlab::Conflict::Parser::UnmergeableFile) + + get :show, + namespace_id: merge_request_with_conflicts.project.namespace.to_param, + project_id: merge_request_with_conflicts.project, + id: merge_request_with_conflicts.iid, + format: 'json' + end + + it 'returns a 200 status code' do + expect(response).to have_http_status(:ok) + end + + it 'returns JSON with a message' do + expect(json_response.keys).to contain_exactly('message', 'type') + end + end + + context 'with valid conflicts' do + before do + get :show, + namespace_id: merge_request_with_conflicts.project.namespace.to_param, + project_id: merge_request_with_conflicts.project, + id: merge_request_with_conflicts.iid, + format: 'json' + end + + it 'matches the schema' do + expect(response).to match_response_schema('conflicts') + end + + it 'includes meta info about the MR' do + expect(json_response['commit_message']).to include('Merge branch') + expect(json_response['commit_sha']).to match(/\h{40}/) + expect(json_response['source_branch']).to eq(merge_request_with_conflicts.source_branch) + expect(json_response['target_branch']).to eq(merge_request_with_conflicts.target_branch) + end + + it 'includes each file that has conflicts' do + filenames = json_response['files'].map { |file| file['new_path'] } + + expect(filenames).to contain_exactly('files/ruby/popen.rb', 'files/ruby/regex.rb') + end + + it 'splits files into sections with lines' do + json_response['files'].each do |file| + file['sections'].each do |section| + expect(section).to include('conflict', 'lines') + + section['lines'].each do |line| + if section['conflict'] + expect(line['type']).to be_in(%w(old new)) + expect(line.values_at('old_line', 'new_line')).to contain_exactly(nil, a_kind_of(Integer)) + else + if line['type'].nil? + expect(line['old_line']).not_to eq(nil) + expect(line['new_line']).not_to eq(nil) + else + expect(line['type']).to eq('match') + expect(line['old_line']).to eq(nil) + expect(line['new_line']).to eq(nil) + end + end + end + end + end + end + + it 'has unique section IDs across files' do + section_ids = json_response['files'].flat_map do |file| + file['sections'].map { |section| section['id'] }.compact + end + + expect(section_ids.uniq).to eq(section_ids) + end + end + end + + describe 'GET conflict_for_path' do + def conflict_for_path(path) + get :conflict_for_path, + namespace_id: merge_request_with_conflicts.project.namespace.to_param, + project_id: merge_request_with_conflicts.project, + id: merge_request_with_conflicts.iid, + old_path: path, + new_path: path, + format: 'json' + end + + context 'when the conflicts cannot be resolved in the UI' do + before do + allow_any_instance_of(Gitlab::Conflict::Parser) + .to receive(:parse).and_raise(Gitlab::Conflict::Parser::UnmergeableFile) + + conflict_for_path('files/ruby/regex.rb') + end + + it 'returns a 404 status code' do + expect(response).to have_http_status(:not_found) + end + end + + context 'when the file does not exist cannot be resolved in the UI' do + before do + conflict_for_path('files/ruby/regexp.rb') + end + + it 'returns a 404 status code' do + expect(response).to have_http_status(:not_found) + end + end + + context 'with an existing file' do + let(:path) { 'files/ruby/regex.rb' } + + before do + conflict_for_path(path) + end + + it 'returns a 200 status code' do + expect(response).to have_http_status(:ok) + end + + it 'returns the file in JSON format' do + content = MergeRequests::Conflicts::ListService.new(merge_request_with_conflicts) + .file_for_path(path, path) + .content + + expect(json_response).to include('old_path' => path, + 'new_path' => path, + 'blob_icon' => 'file-text-o', + 'blob_path' => a_string_ending_with(path), + 'blob_ace_mode' => 'ruby', + 'content' => content) + end + end + end + + context 'POST resolve_conflicts' do + let!(:original_head_sha) { merge_request_with_conflicts.diff_head_sha } + + def resolve_conflicts(files) + post :resolve_conflicts, + namespace_id: merge_request_with_conflicts.project.namespace.to_param, + project_id: merge_request_with_conflicts.project, + id: merge_request_with_conflicts.iid, + format: 'json', + files: files, + commit_message: 'Commit message' + end + + context 'with valid params' do + before do + resolved_files = [ + { + 'new_path' => 'files/ruby/popen.rb', + 'old_path' => 'files/ruby/popen.rb', + 'sections' => { + '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head' + } + }, { + 'new_path' => 'files/ruby/regex.rb', + 'old_path' => 'files/ruby/regex.rb', + 'sections' => { + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head', + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin', + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin' + } + } + ] + + resolve_conflicts(resolved_files) + end + + it 'creates a new commit on the branch' do + expect(original_head_sha).not_to eq(merge_request_with_conflicts.source_branch_head.sha) + expect(merge_request_with_conflicts.source_branch_head.message).to include('Commit message') + end + + it 'returns an OK response' do + expect(response).to have_http_status(:ok) + end + end + + context 'when sections are missing' do + before do + resolved_files = [ + { + 'new_path' => 'files/ruby/popen.rb', + 'old_path' => 'files/ruby/popen.rb', + 'sections' => { + '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head' + } + }, { + 'new_path' => 'files/ruby/regex.rb', + 'old_path' => 'files/ruby/regex.rb', + 'sections' => { + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head' + } + } + ] + + resolve_conflicts(resolved_files) + end + + it 'returns a 400 error' do + expect(response).to have_http_status(:bad_request) + end + + it 'has a message with the name of the first missing section' do + expect(json_response['message']).to include('6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21') + end + + it 'does not create a new commit' do + expect(original_head_sha).to eq(merge_request_with_conflicts.source_branch_head.sha) + end + end + + context 'when files are missing' do + before do + resolved_files = [ + { + 'new_path' => 'files/ruby/regex.rb', + 'old_path' => 'files/ruby/regex.rb', + 'sections' => { + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head', + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin', + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin' + } + } + ] + + resolve_conflicts(resolved_files) + end + + it 'returns a 400 error' do + expect(response).to have_http_status(:bad_request) + end + + it 'has a message with the name of the missing file' do + expect(json_response['message']).to include('files/ruby/popen.rb') + end + + it 'does not create a new commit' do + expect(original_head_sha).to eq(merge_request_with_conflicts.source_branch_head.sha) + end + end + + context 'when a file has identical content to the conflict' do + before do + content = MergeRequests::Conflicts::ListService.new(merge_request_with_conflicts) + .file_for_path('files/ruby/popen.rb', 'files/ruby/popen.rb') + .content + + resolved_files = [ + { + 'new_path' => 'files/ruby/popen.rb', + 'old_path' => 'files/ruby/popen.rb', + 'content' => content + }, { + 'new_path' => 'files/ruby/regex.rb', + 'old_path' => 'files/ruby/regex.rb', + 'sections' => { + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head', + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin', + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin' + } + } + ] + + resolve_conflicts(resolved_files) + end + + it 'returns a 400 error' do + expect(response).to have_http_status(:bad_request) + end + + it 'has a message with the path of the problem file' do + expect(json_response['message']).to include('files/ruby/popen.rb') + end + + it 'does not create a new commit' do + expect(original_head_sha).to eq(merge_request_with_conflicts.source_branch_head.sha) + end + end + end +end diff --git a/spec/controllers/projects/merge_requests/creations_controller_spec.rb b/spec/controllers/projects/merge_requests/creations_controller_spec.rb new file mode 100644 index 00000000000..f9d8f0f5fcf --- /dev/null +++ b/spec/controllers/projects/merge_requests/creations_controller_spec.rb @@ -0,0 +1,120 @@ +require 'spec_helper' + +describe Projects::MergeRequests::CreationsController do + let(:project) { create(:project) } + let(:user) { project.owner } + let(:fork_project) { create(:forked_project_with_submodules) } + + before do + fork_project.team << [user, :master] + + sign_in(user) + end + + describe 'GET new' do + context 'merge request that removes a submodule' do + render_views + + it 'renders new merge request widget template' do + get :new, + namespace_id: fork_project.namespace.to_param, + project_id: fork_project, + merge_request: { + source_branch: 'remove-submodule', + target_branch: 'master' + } + + expect(response).to be_success + end + end + end + + describe 'GET pipelines' do + before do + create(:ci_pipeline, sha: fork_project.commit('remove-submodule').id, + ref: 'remove-submodule', + project: fork_project) + end + + it 'renders JSON including serialized pipelines' do + get :pipelines, + namespace_id: fork_project.namespace.to_param, + project_id: fork_project, + merge_request: { + source_branch: 'remove-submodule', + target_branch: 'master' + }, + format: :json + + expect(response).to be_ok + expect(json_response).to have_key 'pipelines' + expect(json_response['pipelines']).not_to be_empty + end + end + + describe 'GET diff_for_path' do + def diff_for_path(extra_params = {}) + params = { + namespace_id: project.namespace.to_param, + project_id: project, + format: 'json' + } + + get :diff_for_path, params.merge(extra_params) + end + + let(:existing_path) { 'files/ruby/feature.rb' } + + context 'when both branches are in the same project' do + it 'disables diff notes' do + diff_for_path(old_path: existing_path, new_path: existing_path, merge_request: { source_branch: 'feature', target_branch: 'master' }) + + expect(assigns(:diff_notes_disabled)).to be_truthy + end + + it 'only renders the diffs for the path given' do + expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs| + expect(diffs.diff_files.map(&:new_path)).to contain_exactly(existing_path) + meth.call(diffs) + end + + diff_for_path(old_path: existing_path, new_path: existing_path, merge_request: { source_branch: 'feature', target_branch: 'master' }) + end + end + + context 'when the source branch is in a different project to the target' do + let(:other_project) { create(:project) } + + before do + other_project.team << [user, :master] + end + + context 'when the path exists in the diff' do + it 'disables diff notes' do + diff_for_path(old_path: existing_path, new_path: existing_path, merge_request: { source_project: other_project, source_branch: 'feature', target_branch: 'master' }) + + expect(assigns(:diff_notes_disabled)).to be_truthy + end + + it 'only renders the diffs for the path given' do + expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs| + expect(diffs.diff_files.map(&:new_path)).to contain_exactly(existing_path) + meth.call(diffs) + end + + diff_for_path(old_path: existing_path, new_path: existing_path, merge_request: { source_project: other_project, source_branch: 'feature', target_branch: 'master' }) + end + end + + context 'when the path does not exist in the diff' do + before do + diff_for_path(old_path: 'files/ruby/nopen.rb', new_path: 'files/ruby/nopen.rb', merge_request: { source_project: other_project, source_branch: 'feature', target_branch: 'master' }) + end + + it 'returns a 404' do + expect(response).to have_http_status(404) + end + end + end + end +end diff --git a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb new file mode 100644 index 00000000000..53fe2bdb189 --- /dev/null +++ b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb @@ -0,0 +1,160 @@ +require 'spec_helper' + +describe Projects::MergeRequests::DiffsController do + let(:project) { create(:project) } + let(:user) { project.owner } + let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) } + + before do + sign_in(user) + end + + describe 'GET show' do + def go(extra_params = {}) + params = { + namespace_id: project.namespace.to_param, + project_id: project, + id: merge_request.iid, + format: 'json' + } + + get :show, params.merge(extra_params) + end + + context 'with default params' do + context 'for the same project' do + before do + go + end + + it 'renders the diffs template to a string' do + expect(response).to render_template('projects/merge_requests/diffs/_diffs') + expect(json_response).to have_key('html') + end + end + + context 'with forked projects with submodules' do + render_views + + let(:project) { create(:project) } + let(:fork_project) { create(:forked_project_with_submodules) } + let(:merge_request) { create(:merge_request_with_diffs, source_project: fork_project, source_branch: 'add-submodule-version-bump', target_branch: 'master', target_project: project) } + + before do + fork_project.build_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id) + fork_project.save + merge_request.reload + go + end + + it 'renders' do + expect(response).to be_success + expect(response.body).to have_content('Subproject commit') + end + end + end + + context 'with ignore_whitespace_change' do + before do + go(w: 1) + end + + it 'renders the diffs template to a string' do + expect(response).to render_template('projects/merge_requests/diffs/_diffs') + expect(json_response).to have_key('html') + end + end + + context 'with view' do + before do + go(view: 'parallel') + end + + it 'saves the preferred diff view in a cookie' do + expect(response.cookies['diff_view']).to eq('parallel') + end + end + end + + describe 'GET diff_for_path' do + def diff_for_path(extra_params = {}) + params = { + namespace_id: project.namespace.to_param, + project_id: project, + id: merge_request.iid, + format: 'json' + } + + get :diff_for_path, params.merge(extra_params) + end + + let(:existing_path) { 'files/ruby/popen.rb' } + + context 'when the merge request exists' do + context 'when the user can view the merge request' do + context 'when the path exists in the diff' do + it 'enables diff notes' do + diff_for_path(old_path: existing_path, new_path: existing_path) + + expect(assigns(:diff_notes_disabled)).to be_falsey + expect(assigns(:new_diff_note_attrs)).to eq(noteable_type: 'MergeRequest', + noteable_id: merge_request.id) + end + + it 'only renders the diffs for the path given' do + expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs| + expect(diffs.diff_files.map(&:new_path)).to contain_exactly(existing_path) + meth.call(diffs) + end + + diff_for_path(old_path: existing_path, new_path: existing_path) + end + end + + context 'when the path does not exist in the diff' do + before do + diff_for_path(old_path: 'files/ruby/nopen.rb', new_path: 'files/ruby/nopen.rb') + end + + it 'returns a 404' do + expect(response).to have_http_status(404) + end + end + end + + context 'when the user cannot view the merge request' do + before do + project.team.truncate + diff_for_path(old_path: existing_path, new_path: existing_path) + end + + it 'returns a 404' do + expect(response).to have_http_status(404) + end + end + end + + context 'when the merge request does not exist' do + before do + diff_for_path(id: merge_request.iid.succ, old_path: existing_path, new_path: existing_path) + end + + it 'returns a 404' do + expect(response).to have_http_status(404) + end + end + + context 'when the merge request belongs to a different project' do + let(:other_project) { create(:empty_project) } + + before do + other_project.team << [user, :master] + diff_for_path(old_path: existing_path, new_path: existing_path, project_id: other_project) + end + + it 'returns a 404' do + expect(response).to have_http_status(404) + end + end + end +end diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 6817c2652fd..6f9ce60cf75 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -14,53 +14,6 @@ describe Projects::MergeRequestsController do sign_in(user) end - describe 'GET new' do - context 'merge request that removes a submodule' do - render_views - - let(:fork_project) { create(:forked_project_with_submodules) } - - before do - fork_project.team << [user, :master] - end - - context 'when rendering HTML response' do - it 'renders new merge request widget template' do - submit_new_merge_request - - expect(response).to be_success - end - end - - context 'when rendering JSON response' do - before do - create(:ci_pipeline, sha: fork_project.commit('remove-submodule').id, - ref: 'remove-submodule', - project: fork_project) - end - - it 'renders JSON including serialized pipelines' do - submit_new_merge_request(format: :json) - - expect(response).to be_ok - expect(json_response).to have_key 'pipelines' - expect(json_response['pipelines']).not_to be_empty - end - end - end - - def submit_new_merge_request(format: :html) - get :new, - namespace_id: fork_project.namespace.to_param, - project_id: fork_project, - merge_request: { - source_branch: 'remove-submodule', - target_branch: 'master' - }, - format: format - end - end - describe 'GET commit_change_content' do it 'renders commit_change_content template' do get :commit_change_content, @@ -497,234 +450,6 @@ describe Projects::MergeRequestsController do end end - describe 'GET diffs' do - def go(extra_params = {}) - params = { - namespace_id: project.namespace.to_param, - project_id: project, - id: merge_request.iid - } - - get :diffs, params.merge(extra_params) - end - - it_behaves_like "loads labels", :diffs - - context 'with default params' do - context 'as html' do - before do - go(format: 'html') - end - - it 'renders the diff template' do - expect(response).to render_template('diffs') - end - end - - context 'as json' do - before do - go(format: 'json') - end - - it 'renders the diffs template to a string' do - expect(response).to render_template('projects/merge_requests/show/_diffs') - expect(json_response).to have_key('html') - end - end - - context 'with forked projects with submodules' do - render_views - - let(:project) { create(:project) } - let(:fork_project) { create(:forked_project_with_submodules) } - let(:merge_request) { create(:merge_request_with_diffs, source_project: fork_project, source_branch: 'add-submodule-version-bump', target_branch: 'master', target_project: project) } - - before do - fork_project.build_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id) - fork_project.save - merge_request.reload - go(format: 'json') - end - - it 'renders' do - expect(response).to be_success - expect(response.body).to have_content('Subproject commit') - end - end - end - - context 'with ignore_whitespace_change' do - context 'as html' do - before do - go(format: 'html', w: 1) - end - - it 'renders the diff template' do - expect(response).to render_template('diffs') - end - end - - context 'as json' do - before do - go(format: 'json', w: 1) - end - - it 'renders the diffs template to a string' do - expect(response).to render_template('projects/merge_requests/show/_diffs') - expect(json_response).to have_key('html') - end - end - end - - context 'with view' do - before do - go(view: 'parallel') - end - - it 'saves the preferred diff view in a cookie' do - expect(response.cookies['diff_view']).to eq('parallel') - end - end - end - - describe 'GET diff_for_path' do - def diff_for_path(extra_params = {}) - params = { - namespace_id: project.namespace.to_param, - project_id: project - } - - get :diff_for_path, params.merge(extra_params) - end - - context 'when an ID param is passed' do - let(:existing_path) { 'files/ruby/popen.rb' } - - context 'when the merge request exists' do - context 'when the user can view the merge request' do - context 'when the path exists in the diff' do - it 'enables diff notes' do - diff_for_path(id: merge_request.iid, old_path: existing_path, new_path: existing_path) - - expect(assigns(:diff_notes_disabled)).to be_falsey - expect(assigns(:new_diff_note_attrs)).to eq(noteable_type: 'MergeRequest', - noteable_id: merge_request.id) - end - - it 'only renders the diffs for the path given' do - expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs| - expect(diffs.diff_files.map(&:new_path)).to contain_exactly(existing_path) - meth.call(diffs) - end - - diff_for_path(id: merge_request.iid, old_path: existing_path, new_path: existing_path) - end - end - - context 'when the path does not exist in the diff' do - before do - diff_for_path(id: merge_request.iid, old_path: 'files/ruby/nopen.rb', new_path: 'files/ruby/nopen.rb') - end - - it 'returns a 404' do - expect(response).to have_http_status(404) - end - end - end - - context 'when the user cannot view the merge request' do - before do - project.team.truncate - diff_for_path(id: merge_request.iid, old_path: existing_path, new_path: existing_path) - end - - it 'returns a 404' do - expect(response).to have_http_status(404) - end - end - end - - context 'when the merge request does not exist' do - before do - diff_for_path(id: merge_request.iid.succ, old_path: existing_path, new_path: existing_path) - end - - it 'returns a 404' do - expect(response).to have_http_status(404) - end - end - - context 'when the merge request belongs to a different project' do - let(:other_project) { create(:empty_project) } - - before do - other_project.team << [user, :master] - diff_for_path(id: merge_request.iid, old_path: existing_path, new_path: existing_path, project_id: other_project) - end - - it 'returns a 404' do - expect(response).to have_http_status(404) - end - end - end - - context 'when source and target params are passed' do - let(:existing_path) { 'files/ruby/feature.rb' } - - context 'when both branches are in the same project' do - it 'disables diff notes' do - diff_for_path(old_path: existing_path, new_path: existing_path, merge_request: { source_branch: 'feature', target_branch: 'master' }) - - expect(assigns(:diff_notes_disabled)).to be_truthy - end - - it 'only renders the diffs for the path given' do - expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs| - expect(diffs.diff_files.map(&:new_path)).to contain_exactly(existing_path) - meth.call(diffs) - end - - diff_for_path(old_path: existing_path, new_path: existing_path, merge_request: { source_branch: 'feature', target_branch: 'master' }) - end - end - - context 'when the source branch is in a different project to the target' do - let(:other_project) { create(:project) } - - before do - other_project.team << [user, :master] - end - - context 'when the path exists in the diff' do - it 'disables diff notes' do - diff_for_path(old_path: existing_path, new_path: existing_path, merge_request: { source_project: other_project, source_branch: 'feature', target_branch: 'master' }) - - expect(assigns(:diff_notes_disabled)).to be_truthy - end - - it 'only renders the diffs for the path given' do - expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs| - expect(diffs.diff_files.map(&:new_path)).to contain_exactly(existing_path) - meth.call(diffs) - end - - diff_for_path(old_path: existing_path, new_path: existing_path, merge_request: { source_project: other_project, source_branch: 'feature', target_branch: 'master' }) - end - end - - context 'when the path does not exist in the diff' do - before do - diff_for_path(old_path: 'files/ruby/nopen.rb', new_path: 'files/ruby/nopen.rb', merge_request: { source_project: other_project, source_branch: 'feature', target_branch: 'master' }) - end - - it 'returns a 404' do - expect(response).to have_http_status(404) - end - end - end - end - end - describe 'GET commits' do def go(format: 'html') get :commits, @@ -734,23 +459,11 @@ describe Projects::MergeRequestsController do format: format end - it_behaves_like "loads labels", :commits + it 'renders the commits template to a string' do + go format: 'json' - context 'as html' do - it 'renders the show template' do - go - - expect(response).to render_template('show') - end - end - - context 'as json' do - it 'renders the commits template to a string' do - go format: 'json' - - expect(response).to render_template('projects/merge_requests/show/_commits') - expect(json_response).to have_key('html') - end + expect(response).to render_template('projects/merge_requests/_commits') + expect(json_response).to have_key('html') end end @@ -759,106 +472,16 @@ describe Projects::MergeRequestsController do create(:ci_pipeline, project: merge_request.source_project, ref: merge_request.source_branch, sha: merge_request.diff_head_sha) - end - - context 'when using HTML format' do - it_behaves_like "loads labels", :pipelines - end - context 'when using JSON format' do - before do - get :pipelines, - namespace_id: project.namespace.to_param, - project_id: project, - id: merge_request.iid, - format: :json - end - - it 'responds with serialized pipelines' do - expect(json_response).not_to be_empty - end - end - end - - describe 'GET conflicts' do - context 'when the conflicts cannot be resolved in the UI' do - before do - allow_any_instance_of(Gitlab::Conflict::Parser) - .to receive(:parse).and_raise(Gitlab::Conflict::Parser::UnmergeableFile) - - get :conflicts, - namespace_id: merge_request_with_conflicts.project.namespace.to_param, - project_id: merge_request_with_conflicts.project, - id: merge_request_with_conflicts.iid, - format: 'json' - end - - it 'returns a 200 status code' do - expect(response).to have_http_status(:ok) - end - - it 'returns JSON with a message' do - expect(json_response.keys).to contain_exactly('message', 'type') - end + get :pipelines, + namespace_id: project.namespace.to_param, + project_id: project, + id: merge_request.iid, + format: :json end - context 'with valid conflicts' do - before do - get :conflicts, - namespace_id: merge_request_with_conflicts.project.namespace.to_param, - project_id: merge_request_with_conflicts.project, - id: merge_request_with_conflicts.iid, - format: 'json' - end - - it 'matches the schema' do - expect(response).to match_response_schema('conflicts') - end - - it 'includes meta info about the MR' do - expect(json_response['commit_message']).to include('Merge branch') - expect(json_response['commit_sha']).to match(/\h{40}/) - expect(json_response['source_branch']).to eq(merge_request_with_conflicts.source_branch) - expect(json_response['target_branch']).to eq(merge_request_with_conflicts.target_branch) - end - - it 'includes each file that has conflicts' do - filenames = json_response['files'].map { |file| file['new_path'] } - - expect(filenames).to contain_exactly('files/ruby/popen.rb', 'files/ruby/regex.rb') - end - - it 'splits files into sections with lines' do - json_response['files'].each do |file| - file['sections'].each do |section| - expect(section).to include('conflict', 'lines') - - section['lines'].each do |line| - if section['conflict'] - expect(line['type']).to be_in(%w(old new)) - expect(line.values_at('old_line', 'new_line')).to contain_exactly(nil, a_kind_of(Integer)) - else - if line['type'].nil? - expect(line['old_line']).not_to eq(nil) - expect(line['new_line']).not_to eq(nil) - else - expect(line['type']).to eq('match') - expect(line['old_line']).to eq(nil) - expect(line['new_line']).to eq(nil) - end - end - end - end - end - end - - it 'has unique section IDs across files' do - section_ids = json_response['files'].flat_map do |file| - file['sections'].map { |section| section['id'] }.compact - end - - expect(section_ids.uniq).to eq(section_ids) - end + it 'responds with serialized pipelines' do + expect(json_response).not_to be_empty end end @@ -913,215 +536,6 @@ describe Projects::MergeRequestsController do end end - describe 'GET conflict_for_path' do - def conflict_for_path(path) - get :conflict_for_path, - namespace_id: merge_request_with_conflicts.project.namespace.to_param, - project_id: merge_request_with_conflicts.project, - id: merge_request_with_conflicts.iid, - old_path: path, - new_path: path, - format: 'json' - end - - context 'when the conflicts cannot be resolved in the UI' do - before do - allow_any_instance_of(Gitlab::Conflict::Parser) - .to receive(:parse).and_raise(Gitlab::Conflict::Parser::UnmergeableFile) - - conflict_for_path('files/ruby/regex.rb') - end - - it 'returns a 404 status code' do - expect(response).to have_http_status(:not_found) - end - end - - context 'when the file does not exist cannot be resolved in the UI' do - before do - conflict_for_path('files/ruby/regexp.rb') - end - - it 'returns a 404 status code' do - expect(response).to have_http_status(:not_found) - end - end - - context 'with an existing file' do - let(:path) { 'files/ruby/regex.rb' } - - before do - conflict_for_path(path) - end - - it 'returns a 200 status code' do - expect(response).to have_http_status(:ok) - end - - it 'returns the file in JSON format' do - content = MergeRequests::Conflicts::ListService.new(merge_request_with_conflicts) - .file_for_path(path, path) - .content - - expect(json_response).to include('old_path' => path, - 'new_path' => path, - 'blob_icon' => 'file-text-o', - 'blob_path' => a_string_ending_with(path), - 'blob_ace_mode' => 'ruby', - 'content' => content) - end - end - end - - context 'POST resolve_conflicts' do - let!(:original_head_sha) { merge_request_with_conflicts.diff_head_sha } - - def resolve_conflicts(files) - post :resolve_conflicts, - namespace_id: merge_request_with_conflicts.project.namespace.to_param, - project_id: merge_request_with_conflicts.project, - id: merge_request_with_conflicts.iid, - format: 'json', - files: files, - commit_message: 'Commit message' - end - - context 'with valid params' do - before do - resolved_files = [ - { - 'new_path' => 'files/ruby/popen.rb', - 'old_path' => 'files/ruby/popen.rb', - 'sections' => { - '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head' - } - }, { - 'new_path' => 'files/ruby/regex.rb', - 'old_path' => 'files/ruby/regex.rb', - 'sections' => { - '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head', - '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin', - '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin' - } - } - ] - - resolve_conflicts(resolved_files) - end - - it 'creates a new commit on the branch' do - expect(original_head_sha).not_to eq(merge_request_with_conflicts.source_branch_head.sha) - expect(merge_request_with_conflicts.source_branch_head.message).to include('Commit message') - end - - it 'returns an OK response' do - expect(response).to have_http_status(:ok) - end - end - - context 'when sections are missing' do - before do - resolved_files = [ - { - 'new_path' => 'files/ruby/popen.rb', - 'old_path' => 'files/ruby/popen.rb', - 'sections' => { - '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head' - } - }, { - 'new_path' => 'files/ruby/regex.rb', - 'old_path' => 'files/ruby/regex.rb', - 'sections' => { - '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head' - } - } - ] - - resolve_conflicts(resolved_files) - end - - it 'returns a 400 error' do - expect(response).to have_http_status(:bad_request) - end - - it 'has a message with the name of the first missing section' do - expect(json_response['message']).to include('6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21') - end - - it 'does not create a new commit' do - expect(original_head_sha).to eq(merge_request_with_conflicts.source_branch_head.sha) - end - end - - context 'when files are missing' do - before do - resolved_files = [ - { - 'new_path' => 'files/ruby/regex.rb', - 'old_path' => 'files/ruby/regex.rb', - 'sections' => { - '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head', - '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin', - '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin' - } - } - ] - - resolve_conflicts(resolved_files) - end - - it 'returns a 400 error' do - expect(response).to have_http_status(:bad_request) - end - - it 'has a message with the name of the missing file' do - expect(json_response['message']).to include('files/ruby/popen.rb') - end - - it 'does not create a new commit' do - expect(original_head_sha).to eq(merge_request_with_conflicts.source_branch_head.sha) - end - end - - context 'when a file has identical content to the conflict' do - before do - content = MergeRequests::Conflicts::ListService.new(merge_request_with_conflicts) - .file_for_path('files/ruby/popen.rb', 'files/ruby/popen.rb') - .content - - resolved_files = [ - { - 'new_path' => 'files/ruby/popen.rb', - 'old_path' => 'files/ruby/popen.rb', - 'content' => content - }, { - 'new_path' => 'files/ruby/regex.rb', - 'old_path' => 'files/ruby/regex.rb', - 'sections' => { - '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head', - '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin', - '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin' - } - } - ] - - resolve_conflicts(resolved_files) - end - - it 'returns a 400 error' do - expect(response).to have_http_status(:bad_request) - end - - it 'has a message with the path of the problem file' do - expect(json_response['message']).to include('files/ruby/popen.rb') - end - - it 'does not create a new commit' do - expect(original_head_sha).to eq(merge_request_with_conflicts.source_branch_head.sha) - end - end - end - describe 'POST assign_related_issues' do let(:issue1) { create(:issue, project: project) } let(:issue2) { create(:issue, project: project) } diff --git a/spec/controllers/projects/pipeline_schedules_controller_spec.rb b/spec/controllers/projects/pipeline_schedules_controller_spec.rb index f8f95dd9bc8..a8c44d5c313 100644 --- a/spec/controllers/projects/pipeline_schedules_controller_spec.rb +++ b/spec/controllers/projects/pipeline_schedules_controller_spec.rb @@ -84,4 +84,57 @@ describe Projects::PipelineSchedulesController do end end end + + describe 'security' do + include AccessMatchersForController + + describe 'GET edit' do + it { expect { go }.to be_allowed_for(:admin) } + it { expect { go }.to be_allowed_for(:owner).of(project) } + it { expect { go }.to be_allowed_for(:master).of(project) } + it { expect { go }.to be_allowed_for(:developer).of(project) } + it { expect { go }.to be_denied_for(:reporter).of(project) } + it { expect { go }.to be_denied_for(:guest).of(project) } + it { expect { go }.to be_denied_for(:user) } + it { expect { go }.to be_denied_for(:external) } + it { expect { go }.to be_denied_for(:visitor) } + + def go + get :edit, namespace_id: project.namespace.to_param, project_id: project, id: pipeline_schedule.id + end + end + + describe 'GET take_ownership' do + it { expect { go }.to be_allowed_for(:admin) } + it { expect { go }.to be_allowed_for(:owner).of(project) } + it { expect { go }.to be_allowed_for(:master).of(project) } + it { expect { go }.to be_allowed_for(:developer).of(project) } + it { expect { go }.to be_denied_for(:reporter).of(project) } + it { expect { go }.to be_denied_for(:guest).of(project) } + it { expect { go }.to be_denied_for(:user) } + it { expect { go }.to be_denied_for(:external) } + it { expect { go }.to be_denied_for(:visitor) } + + def go + post :take_ownership, namespace_id: project.namespace.to_param, project_id: project, id: pipeline_schedule.id + end + end + + describe 'PUT update' do + it { expect { go }.to be_allowed_for(:admin) } + it { expect { go }.to be_allowed_for(:owner).of(project) } + it { expect { go }.to be_allowed_for(:master).of(project) } + it { expect { go }.to be_allowed_for(:developer).of(project) } + it { expect { go }.to be_denied_for(:reporter).of(project) } + it { expect { go }.to be_denied_for(:guest).of(project) } + it { expect { go }.to be_denied_for(:user) } + it { expect { go }.to be_denied_for(:external) } + it { expect { go }.to be_denied_for(:visitor) } + + def go + put :update, namespace_id: project.namespace.to_param, project_id: project, id: pipeline_schedule.id, + schedule: { description: 'a' } + end + end + end end diff --git a/spec/features/dashboard/todos/todos_spec.rb b/spec/features/dashboard/todos/todos_spec.rb index 24da5db305f..7fa4d198e00 100644 --- a/spec/features/dashboard/todos/todos_spec.rb +++ b/spec/features/dashboard/todos/todos_spec.rb @@ -317,23 +317,6 @@ feature 'Dashboard Todos' do end end - context 'User has a Todo in a project pending deletion' do - before do - deleted_project = create(:project, :public, pending_delete: true) - create(:todo, :mentioned, user: user, project: deleted_project, target: issue, author: author) - create(:todo, :mentioned, user: user, project: deleted_project, target: issue, author: author, state: :done) - sign_in(user) - visit dashboard_todos_path - end - - it 'shows "All done" message' do - within('.todos-count') { expect(page).to have_content '0' } - expect(page).to have_content 'To do 0' - expect(page).to have_content 'Done 0' - expect(page).to have_selector('.todos-all-done', count: 1) - end - end - context 'User has a Build Failed todo' do let!(:todo) { create(:todo, :build_failed, user: user, project: project, author: author) } diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb index 8f7adbccaaa..6a08e50bf5e 100644 --- a/spec/features/merge_requests/create_new_mr_spec.rb +++ b/spec/features/merge_requests/create_new_mr_spec.rb @@ -65,7 +65,7 @@ feature 'Create New Merge Request', feature: true, js: true do it 'does not leak the private project name & namespace' do private_project = create(:project, :private) - visit new_namespace_project_merge_request_path(project.namespace, project, merge_request: { target_project_id: private_project.id }) + visit namespace_project_new_merge_request_path(project.namespace, project, merge_request: { target_project_id: private_project.id }) expect(page).not_to have_content private_project.path_with_namespace expect(page).to have_content project.path_with_namespace @@ -76,7 +76,7 @@ feature 'Create New Merge Request', feature: true, js: true do it 'does not leak the private project name & namespace' do private_project = create(:project, :private) - visit new_namespace_project_merge_request_path(project.namespace, project, merge_request: { source_project_id: private_project.id }) + visit namespace_project_new_merge_request_path(project.namespace, project, merge_request: { source_project_id: private_project.id }) expect(page).not_to have_content private_project.path_with_namespace expect(page).to have_content project.path_with_namespace @@ -84,13 +84,13 @@ feature 'Create New Merge Request', feature: true, js: true do end it 'populates source branch button' do - visit new_namespace_project_merge_request_path(project.namespace, project, change_branches: true, merge_request: { target_branch: 'master', source_branch: 'fix' }) + visit namespace_project_new_merge_request_path(project.namespace, project, change_branches: true, merge_request: { target_branch: 'master', source_branch: 'fix' }) expect(find('.js-source-branch')).to have_content('fix') end it 'allows to change the diff view' do - visit new_namespace_project_merge_request_path(project.namespace, project, merge_request: { target_branch: 'master', source_branch: 'fix' }) + visit namespace_project_new_merge_request_path(project.namespace, project, merge_request: { target_branch: 'master', source_branch: 'fix' }) click_link 'Changes' @@ -106,7 +106,7 @@ feature 'Create New Merge Request', feature: true, js: true do end it 'does not allow non-existing branches' do - visit new_namespace_project_merge_request_path(project.namespace, project, merge_request: { target_branch: 'non-exist-target', source_branch: 'non-exist-source' }) + visit namespace_project_new_merge_request_path(project.namespace, project, merge_request: { target_branch: 'non-exist-target', source_branch: 'non-exist-source' }) expect(page).to have_content('The form contains the following errors') expect(page).to have_content('Source branch "non-exist-source" does not exist') @@ -115,7 +115,7 @@ feature 'Create New Merge Request', feature: true, js: true do context 'when a branch contains commits that both delete and add the same image' do it 'renders the diff successfully' do - visit new_namespace_project_merge_request_path(project.namespace, project, merge_request: { target_branch: 'master', source_branch: 'deleted-image-test' }) + visit namespace_project_new_merge_request_path(project.namespace, project, merge_request: { target_branch: 'master', source_branch: 'deleted-image-test' }) click_link "Changes" @@ -125,7 +125,7 @@ feature 'Create New Merge Request', feature: true, js: true do # Isolates a regression (see #24627) it 'does not show error messages on initial form' do - visit new_namespace_project_merge_request_path(project.namespace, project) + visit namespace_project_new_merge_request_path(project.namespace, project) expect(page).not_to have_selector('#error_explanation') expect(page).not_to have_content('The form contains the following error') end @@ -138,7 +138,7 @@ feature 'Create New Merge Request', feature: true, js: true do end it 'shows pipelines for a new merge request' do - visit new_namespace_project_merge_request_path( + visit namespace_project_new_merge_request_path( project.namespace, project, merge_request: { target_branch: 'master', source_branch: 'fix' }) diff --git a/spec/features/merge_requests/form_spec.rb b/spec/features/merge_requests/form_spec.rb index 1996c2fa09a..d03d498ce21 100644 --- a/spec/features/merge_requests/form_spec.rb +++ b/spec/features/merge_requests/form_spec.rb @@ -23,7 +23,7 @@ describe 'New/edit merge request', feature: true, js: true do context 'new merge request' do before do - visit new_namespace_project_merge_request_path( + visit namespace_project_new_merge_request_path( project.namespace, project, merge_request: { @@ -182,7 +182,7 @@ describe 'New/edit merge request', feature: true, js: true do context 'new merge request' do before do - visit new_namespace_project_merge_request_path( + visit namespace_project_new_merge_request_path( fork_project.namespace, fork_project, merge_request: { diff --git a/spec/features/merge_requests/user_uses_slash_commands_spec.rb b/spec/features/merge_requests/user_uses_slash_commands_spec.rb index 71aa71e380e..a1f123f15ec 100644 --- a/spec/features/merge_requests/user_uses_slash_commands_spec.rb +++ b/spec/features/merge_requests/user_uses_slash_commands_spec.rb @@ -131,7 +131,7 @@ feature 'Merge Requests > User uses quick actions', feature: true, js: true do end it 'changes target_branch in new merge_request' do - visit new_namespace_project_merge_request_path(another_project.namespace, another_project, new_url_opts) + visit namespace_project_new_merge_request_path(another_project.namespace, another_project, new_url_opts) fill_in "merge_request_title", with: 'My brand new feature' fill_in "merge_request_description", with: "le feature \n/target_branch fix\nFeature description:" diff --git a/spec/features/merge_requests/widget_spec.rb b/spec/features/merge_requests/widget_spec.rb index 3ac1f603de6..d8e9b949204 100644 --- a/spec/features/merge_requests/widget_spec.rb +++ b/spec/features/merge_requests/widget_spec.rb @@ -12,7 +12,7 @@ describe 'Merge request', :feature, :js do context 'new merge request' do before do - visit new_namespace_project_merge_request_path( + visit namespace_project_new_merge_request_path( project.namespace, project, merge_request: { diff --git a/spec/features/merge_requests/wip_message_spec.rb b/spec/features/merge_requests/wip_message_spec.rb index 72d001bf408..0e304ba50af 100644 --- a/spec/features/merge_requests/wip_message_spec.rb +++ b/spec/features/merge_requests/wip_message_spec.rb @@ -11,7 +11,7 @@ feature 'Work In Progress help message', feature: true do context 'with WIP commits' do it 'shows a specific WIP hint' do - visit new_namespace_project_merge_request_path( + visit namespace_project_new_merge_request_path( project.namespace, project, merge_request: { @@ -32,7 +32,7 @@ feature 'Work In Progress help message', feature: true do context 'without WIP commits' do it 'shows the regular WIP message' do - visit new_namespace_project_merge_request_path( + visit namespace_project_new_merge_request_path( project.namespace, project, merge_request: { diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb index 8694366de35..0050864d305 100644 --- a/spec/features/projects/branches_spec.rb +++ b/spec/features/projects/branches_spec.rb @@ -21,10 +21,48 @@ describe 'Branches', feature: true do it 'shows all the branches' do visit namespace_project_branches_path(project.namespace, project) - repository.branches { |branch| expect(page).to have_content("#{branch.name}") } + repository.branches_sorted_by(:name).first(20).each do |branch| + expect(page).to have_content("#{branch.name}") + end expect(page).to have_content("Protected branches can be managed in project settings") end + it 'sorts the branches by name' do + visit namespace_project_branches_path(project.namespace, project) + + click_button "Name" # Open sorting dropdown + click_link "Name" + + sorted = repository.branches_sorted_by(:name).first(20).map do |branch| + Regexp.escape(branch.name) + end + expect(page).to have_content(/#{sorted.join(".*")}/) + end + + it 'sorts the branches by last updated' do + visit namespace_project_branches_path(project.namespace, project) + + click_button "Name" # Open sorting dropdown + click_link "Last updated" + + sorted = repository.branches_sorted_by(:updated_desc).first(20).map do |branch| + Regexp.escape(branch.name) + end + expect(page).to have_content(/#{sorted.join(".*")}/) + end + + it 'sorts the branches by oldest updated' do + visit namespace_project_branches_path(project.namespace, project) + + click_button "Name" # Open sorting dropdown + click_link "Oldest updated" + + sorted = repository.branches_sorted_by(:updated_asc).first(20).map do |branch| + Regexp.escape(branch.name) + end + expect(page).to have_content(/#{sorted.join(".*")}/) + end + it 'avoids a N+1 query in branches index' do control_count = ActiveRecord::QueryRecorder.new { visit namespace_project_branches_path(project.namespace, project) }.count diff --git a/spec/features/projects/merge_request_button_spec.rb b/spec/features/projects/merge_request_button_spec.rb index 6de8855016d..58054bbbbed 100644 --- a/spec/features/projects/merge_request_button_spec.rb +++ b/spec/features/projects/merge_request_button_spec.rb @@ -23,7 +23,7 @@ feature 'Merge Request button', feature: true do end it 'shows Create merge request button' do - href = new_namespace_project_merge_request_path(project.namespace, + href = namespace_project_new_merge_request_path(project.namespace, project, merge_request: { source_branch: 'feature', target_branch: 'master' }) @@ -67,7 +67,7 @@ feature 'Merge Request button', feature: true do let(:user) { forked_project.owner } it 'shows Create merge request button' do - href = new_namespace_project_merge_request_path(forked_project.namespace, + href = namespace_project_new_merge_request_path(forked_project.namespace, forked_project, merge_request: { source_branch: 'feature', target_branch: 'master' }) diff --git a/spec/features/projects/merge_requests/list_spec.rb b/spec/features/projects/merge_requests/list_spec.rb index f2a2fd0311f..7ce3156215a 100644 --- a/spec/features/projects/merge_requests/list_spec.rb +++ b/spec/features/projects/merge_requests/list_spec.rb @@ -27,7 +27,7 @@ feature 'Merge Requests List' do it 'empty state should have a create merge request button' do visit namespace_project_merge_requests_path(project.namespace, project) - expect(page).to have_link 'New merge request', href: new_namespace_project_merge_request_path(project.namespace, project) + expect(page).to have_link 'New merge request', href: namespace_project_new_merge_request_path(project.namespace, project) end context 'if there are merge requests' do diff --git a/spec/features/projects/user_create_dir_spec.rb b/spec/features/projects/user_create_dir_spec.rb index f375e1215db..5d0acad3832 100644 --- a/spec/features/projects/user_create_dir_spec.rb +++ b/spec/features/projects/user_create_dir_spec.rb @@ -51,7 +51,7 @@ feature 'New directory creation', feature: true, js: true do expect(page).to have_content 'New Merge Request' expect(page).to have_content "From #{new_branch_name} into master" expect(page).to have_content 'Add new directory' - expect(current_path).to eq(new_namespace_project_merge_request_path(project.namespace, project)) + expect(current_path).to eq(namespace_project_new_merge_request_path(project.namespace, project)) end end end diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb index f33406a40a7..5e26b8bbed6 100644 --- a/spec/features/security/project/internal_access_spec.rb +++ b/spec/features/security/project/internal_access_spec.rb @@ -239,7 +239,7 @@ describe "Internal Project Access", feature: true do end describe "GET /:project_path/merge_requests/new" do - subject { new_namespace_project_merge_request_path(project.namespace, project) } + subject { namespace_project_new_merge_request_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } it { is_expected.to be_allowed_for(:owner).of(project) } diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb index 16a1331b2f3..59655b0c31a 100644 --- a/spec/features/security/project/public_access_spec.rb +++ b/spec/features/security/project/public_access_spec.rb @@ -452,7 +452,7 @@ describe "Public Project Access", feature: true do end describe "GET /:project_path/merge_requests/new" do - subject { new_namespace_project_merge_request_path(project.namespace, project) } + subject { namespace_project_new_merge_request_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } it { is_expected.to be_allowed_for(:owner).of(project) } diff --git a/spec/helpers/submodule_helper_spec.rb b/spec/helpers/submodule_helper_spec.rb index cb727430117..9e561d0f191 100644 --- a/spec/helpers/submodule_helper_spec.rb +++ b/spec/helpers/submodule_helper_spec.rb @@ -170,6 +170,11 @@ describe SubmoduleHelper do expect(result).to eq(["/#{group.path}/test", "/#{group.path}/test/tree/#{commit_id}"]) end + it 'with trailing whitespace' do + result = relative_self_links('../test.git ', commit_id) + expect(result).to eq(["/#{group.path}/test", "/#{group.path}/test/tree/#{commit_id}"]) + end + it 'two levels down' do result = relative_self_links('../../test.git', commit_id) expect(result).to eq(["/#{group.path}/test", "/#{group.path}/test/tree/#{commit_id}"]) diff --git a/spec/javascripts/fixtures/merge_requests.rb b/spec/javascripts/fixtures/merge_requests.rb index daaddd8f390..7e2f364ffa4 100644 --- a/spec/javascripts/fixtures/merge_requests.rb +++ b/spec/javascripts/fixtures/merge_requests.rb @@ -55,27 +55,14 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont render_merge_request(example.description, merge_request) end - it 'merge_requests/inline_changes_tab_with_comments.json' do |example| - create(:diff_note_on_merge_request, project: project, author: admin, position: position, noteable: merge_request) - create(:note_on_merge_request, author: admin, project: project, noteable: merge_request) - render_merge_request(example.description, merge_request, action: :diffs, format: :json) - end - - it 'merge_requests/parallel_changes_tab_with_comments.json' do |example| - create(:diff_note_on_merge_request, project: project, author: admin, position: position, noteable: merge_request) - create(:note_on_merge_request, author: admin, project: project, noteable: merge_request) - render_merge_request(example.description, merge_request, action: :diffs, format: :json, view: 'parallel') - end - private - def render_merge_request(fixture_file_name, merge_request, action: :show, format: :html, view: 'inline') - get action, + def render_merge_request(fixture_file_name, merge_request) + get :show, namespace_id: project.namespace.to_param, project_id: project, id: merge_request.to_param, - format: format, - view: view + format: :html expect(response).to be_success store_frontend_fixture(response, fixture_file_name) diff --git a/spec/javascripts/fixtures/merge_requests_diffs.rb b/spec/javascripts/fixtures/merge_requests_diffs.rb new file mode 100644 index 00000000000..ac5b06ace6d --- /dev/null +++ b/spec/javascripts/fixtures/merge_requests_diffs.rb @@ -0,0 +1,57 @@ + +require 'spec_helper' + +describe Projects::MergeRequests::DiffsController, '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin) } + let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:project) { create(:project, namespace: namespace, path: 'merge-requests-project') } + let(:merge_request) { create(:merge_request, :with_diffs, source_project: project, target_project: project, description: '- [ ] Task List Item') } + let(:path) { "files/ruby/popen.rb" } + let(:position) do + Gitlab::Diff::Position.new( + old_path: path, + new_path: path, + old_line: nil, + new_line: 14, + diff_refs: merge_request.diff_refs + ) + end + + render_views + + before(:all) do + clean_frontend_fixtures('merge_request_diffs/') + end + + before(:each) do + sign_in(admin) + end + + it 'merge_request_diffs/inline_changes_tab_with_comments.json' do |example| + create(:diff_note_on_merge_request, project: project, author: admin, position: position, noteable: merge_request) + create(:note_on_merge_request, author: admin, project: project, noteable: merge_request) + render_merge_request(example.description, merge_request) + end + + it 'merge_request_diffs/parallel_changes_tab_with_comments.json' do |example| + create(:diff_note_on_merge_request, project: project, author: admin, position: position, noteable: merge_request) + create(:note_on_merge_request, author: admin, project: project, noteable: merge_request) + render_merge_request(example.description, merge_request, view: 'parallel') + end + + private + + def render_merge_request(fixture_file_name, merge_request, view: 'inline') + get :show, + namespace_id: project.namespace.to_param, + project_id: project, + id: merge_request.to_param, + format: :json, + view: view + + expect(response).to be_success + store_frontend_fixture(response, fixture_file_name) + end +end diff --git a/spec/javascripts/merge_request_notes_spec.js b/spec/javascripts/merge_request_notes_spec.js index 9e9eb17d439..395dc560671 100644 --- a/spec/javascripts/merge_request_notes_spec.js +++ b/spec/javascripts/merge_request_notes_spec.js @@ -15,7 +15,7 @@ describe('Merge request notes', () => { gl.utils = gl.utils || {}; const discussionTabFixture = 'merge_requests/diff_comment.html.raw'; - const changesTabJsonFixture = 'merge_requests/inline_changes_tab_with_comments.json'; + const changesTabJsonFixture = 'merge_request_diffs/inline_changes_tab_with_comments.json'; preloadFixtures(discussionTabFixture, changesTabJsonFixture); describe('Discussion tab with diff comments', () => { diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js index bb6b5d852d3..49ef21f75de 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -23,8 +23,8 @@ import 'vendor/jquery.scrollTo'; $.extend(stubLocation, defaults, stubs || {}); }; - const inlineChangesTabJsonFixture = 'merge_requests/inline_changes_tab_with_comments.json'; - const parallelChangesTabJsonFixture = 'merge_requests/parallel_changes_tab_with_comments.json'; + const inlineChangesTabJsonFixture = 'merge_request_diffs/inline_changes_tab_with_comments.json'; + const parallelChangesTabJsonFixture = 'merge_request_diffs/parallel_changes_tab_with_comments.json'; preloadFixtures( 'merge_requests/merge_request_with_task_list.html.raw', 'merge_requests/diff_comment.html.raw', @@ -52,14 +52,10 @@ import 'vendor/jquery.scrollTo'; loadFixtures('merge_requests/merge_request_with_task_list.html.raw'); this.subject = this.class.activateTab; }); - it('shows the first tab when action is show', function () { + it('shows the notes tab when action is show', function () { this.subject('show'); expect($('#notes')).toHaveClass('active'); }); - it('shows the notes tab when action is notes', function () { - this.subject('notes'); - expect($('#notes')).toHaveClass('active'); - }); it('shows the commits tab when action is commits', function () { this.subject('commits'); expect($('#commits')).toHaveClass('active'); @@ -161,7 +157,7 @@ import 'vendor/jquery.scrollTo'; setLocation({ pathname: '/foo/bar/merge_requests/1/commits' }); - expect(this.subject('notes')).toBe('/foo/bar/merge_requests/1'); + expect(this.subject('show')).toBe('/foo/bar/merge_requests/1'); expect(this.subject('diffs')).toBe('/foo/bar/merge_requests/1/diffs'); }); @@ -170,7 +166,7 @@ import 'vendor/jquery.scrollTo'; pathname: '/foo/bar/merge_requests/1/diffs' }); - expect(this.subject('notes')).toBe('/foo/bar/merge_requests/1'); + expect(this.subject('show')).toBe('/foo/bar/merge_requests/1'); expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits'); }); @@ -178,7 +174,7 @@ import 'vendor/jquery.scrollTo'; setLocation({ pathname: '/foo/bar/merge_requests/1/diffs.html' }); - expect(this.subject('notes')).toBe('/foo/bar/merge_requests/1'); + expect(this.subject('show')).toBe('/foo/bar/merge_requests/1'); expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits'); }); diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb index 5653cfee686..8813f129ef5 100644 --- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb +++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb @@ -6,6 +6,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameBase, :trunca before do allow(migration).to receive(:say) + TestEnv.clean_test_path end def migration_namespace(namespace) @@ -153,6 +154,30 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameBase, :trunca end end + describe '#perform_rename' do + describe 'for namespaces' do + let(:namespace) { create(:namespace, path: 'the-path') } + it 'renames the path' do + subject.perform_rename(migration_namespace(namespace), 'the-path', 'renamed') + + expect(namespace.reload.path).to eq('renamed') + end + + it 'renames all the routes for the namespace' do + child = create(:group, path: 'child', parent: namespace) + project = create(:project, namespace: child, path: 'the-project') + other_one = create(:namespace, path: 'the-path-is-similar') + + subject.perform_rename(migration_namespace(namespace), 'the-path', 'renamed') + + expect(namespace.reload.route.path).to eq('renamed') + expect(child.reload.route.path).to eq('renamed/child') + expect(project.reload.route.path).to eq('renamed/child/the-project') + expect(other_one.reload.route.path).to eq('the-path-is-similar') + end + end + end + describe '#move_pages' do it 'moves the pages directory' do expect(subject).to receive(:move_folders) @@ -203,4 +228,53 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameBase, :trunca expect(File.exist?(expected_file)).to be(true) end end + + describe '#track_rename', redis: true do + it 'tracks a rename in redis' do + key = 'rename:FakeRenameReservedPathMigrationV1:namespace' + + subject.track_rename('namespace', 'path/to/namespace', 'path/to/renamed') + + old_path, new_path = [nil, nil] + Gitlab::Redis.with do |redis| + rename_info = redis.lpop(key) + old_path, new_path = JSON.parse(rename_info) + end + + expect(old_path).to eq('path/to/namespace') + expect(new_path).to eq('path/to/renamed') + end + end + + describe '#reverts_for_type', redis: true do + it 'yields for each tracked rename' do + subject.track_rename('project', 'old_path', 'new_path') + subject.track_rename('project', 'old_path2', 'new_path2') + subject.track_rename('namespace', 'namespace_path', 'new_namespace_path') + + expect { |b| subject.reverts_for_type('project', &b) } + .to yield_successive_args(%w(old_path2 new_path2), %w(old_path new_path)) + expect { |b| subject.reverts_for_type('namespace', &b) } + .to yield_with_args('namespace_path', 'new_namespace_path') + end + + it 'keeps the revert in redis if it failed' do + subject.track_rename('project', 'old_path', 'new_path') + + subject.reverts_for_type('project') do + raise 'whatever happens, keep going!' + end + + key = 'rename:FakeRenameReservedPathMigrationV1:project' + stored_renames = nil + rename_count = 0 + Gitlab::Redis.with do |redis| + stored_renames = redis.lrange(key, 0, 1) + rename_count = redis.llen(key) + end + + expect(rename_count).to eq(1) + expect(JSON.parse(stored_renames.first)).to eq(%w(old_path new_path)) + end + end end diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb index 8125dedd3fc..803e923b4a5 100644 --- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb +++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb @@ -3,9 +3,11 @@ require 'spec_helper' describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces, :truncate do let(:migration) { FakeRenameReservedPathMigrationV1.new } let(:subject) { described_class.new(['the-path'], migration) } + let(:namespace) { create(:group, name: 'the-path') } before do allow(migration).to receive(:say) + TestEnv.clean_test_path end def migration_namespace(namespace) @@ -137,8 +139,6 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces, : end describe "#rename_namespace" do - let(:namespace) { create(:group, name: 'the-path') } - it 'renames paths & routes for the namespace' do expect(subject).to receive(:rename_path_for_routable) .with(namespace) @@ -149,11 +149,27 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces, : expect(namespace.reload.path).to eq('the-path0') end + it 'tracks the rename' do + expect(subject).to receive(:track_rename) + .with('namespace', 'the-path', 'the-path0') + + subject.rename_namespace(namespace) + end + + it 'renames things related to the namespace' do + expect(subject).to receive(:rename_namespace_dependencies) + .with(namespace, 'the-path', 'the-path0') + + subject.rename_namespace(namespace) + end + end + + describe '#rename_namespace_dependencies' do it "moves the the repository for a project in the namespace" do create(:project, namespace: namespace, path: "the-path-project") expected_repo = File.join(TestEnv.repos_path, "the-path0", "the-path-project.git") - subject.rename_namespace(namespace) + subject.rename_namespace_dependencies(namespace, 'the-path', 'the-path0') expect(File.directory?(expected_repo)).to be(true) end @@ -161,13 +177,13 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces, : it "moves the uploads for the namespace" do expect(subject).to receive(:move_uploads).with("the-path", "the-path0") - subject.rename_namespace(namespace) + subject.rename_namespace_dependencies(namespace, 'the-path', 'the-path0') end it "moves the pages for the namespace" do expect(subject).to receive(:move_pages).with("the-path", "the-path0") - subject.rename_namespace(namespace) + subject.rename_namespace_dependencies(namespace, 'the-path', 'the-path0') end it 'invalidates the markdown cache of related projects' do @@ -175,13 +191,13 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces, : expect(subject).to receive(:remove_cached_html_for_projects).with([project.id]) - subject.rename_namespace(namespace) + subject.rename_namespace_dependencies(namespace, 'the-path', 'the-path0') end it "doesn't rename users for other namespaces" do expect(subject).not_to receive(:rename_user) - subject.rename_namespace(namespace) + subject.rename_namespace_dependencies(namespace, 'the-path', 'the-path0') end it 'renames the username of a namespace for a user' do @@ -189,7 +205,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces, : expect(subject).to receive(:rename_user).with('the-path', 'the-path0') - subject.rename_namespace(user.namespace) + subject.rename_namespace_dependencies(user.namespace, 'the-path', 'the-path0') end end @@ -224,4 +240,50 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces, : subject.rename_namespaces(type: :child) end end + + describe '#revert_renames', redis: true do + it 'renames the routes back to the previous values' do + project = create(:project, path: 'a-project', namespace: namespace) + subject.rename_namespace(namespace) + + expect(subject).to receive(:perform_rename) + .with( + kind_of(Gitlab::Database::RenameReservedPathsMigration::V1::MigrationClasses::Namespace), + 'the-path0', + 'the-path' + ).and_call_original + + subject.revert_renames + + expect(namespace.reload.path).to eq('the-path') + expect(namespace.reload.route.path).to eq('the-path') + expect(project.reload.route.path).to eq('the-path/a-project') + end + + it 'moves the repositories back to their original place' do + project = create(:project, path: 'a-project', namespace: namespace) + project.create_repository + subject.rename_namespace(namespace) + + expected_path = File.join(TestEnv.repos_path, 'the-path', 'a-project.git') + + expect(subject).to receive(:rename_namespace_dependencies) + .with( + kind_of(Gitlab::Database::RenameReservedPathsMigration::V1::MigrationClasses::Namespace), + 'the-path0', + 'the-path' + ).and_call_original + + subject.revert_renames + + expect(File.directory?(expected_path)).to be_truthy + end + + it "doesn't break when the namespace was renamed" do + subject.rename_namespace(namespace) + namespace.update_attributes!(path: 'renamed-afterwards') + + expect { subject.revert_renames }.not_to raise_error + end + end end diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb index 802f77ad430..0e240a5ccf1 100644 --- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb +++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb @@ -3,9 +3,15 @@ require 'spec_helper' describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects, :truncate do let(:migration) { FakeRenameReservedPathMigrationV1.new } let(:subject) { described_class.new(['the-path'], migration) } + let(:project) do + create(:empty_project, + path: 'the-path', + namespace: create(:namespace, path: 'known-parent' )) + end before do allow(migration).to receive(:say) + TestEnv.clean_test_path end describe '#projects_for_paths' do @@ -47,12 +53,6 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects, :tr end describe '#rename_project' do - let(:project) do - create(:empty_project, - path: 'the-path', - namespace: create(:namespace, path: 'known-parent' )) - end - it 'renames path & route for the project' do expect(subject).to receive(:rename_path_for_routable) .with(project) @@ -63,27 +63,42 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects, :tr expect(project.reload.path).to eq('the-path0') end + it 'tracks the rename' do + expect(subject).to receive(:track_rename) + .with('project', 'known-parent/the-path', 'known-parent/the-path0') + + subject.rename_project(project) + end + + it 'renames the folders for the project' do + expect(subject).to receive(:move_project_folders).with(project, 'known-parent/the-path', 'known-parent/the-path0') + + subject.rename_project(project) + end + end + + describe '#move_project_folders' do it 'moves the wiki & the repo' do expect(subject).to receive(:move_repository) .with(project, 'known-parent/the-path.wiki', 'known-parent/the-path0.wiki') expect(subject).to receive(:move_repository) .with(project, 'known-parent/the-path', 'known-parent/the-path0') - subject.rename_project(project) + subject.move_project_folders(project, 'known-parent/the-path', 'known-parent/the-path0') end it 'moves uploads' do expect(subject).to receive(:move_uploads) .with('known-parent/the-path', 'known-parent/the-path0') - subject.rename_project(project) + subject.move_project_folders(project, 'known-parent/the-path', 'known-parent/the-path0') end it 'moves pages' do expect(subject).to receive(:move_pages) .with('known-parent/the-path', 'known-parent/the-path0') - subject.rename_project(project) + subject.move_project_folders(project, 'known-parent/the-path', 'known-parent/the-path0') end end @@ -99,4 +114,47 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects, :tr expect(File.directory?(expected_path)).to be(true) end end + + describe '#revert_renames', redis: true do + it 'renames the routes back to the previous values' do + subject.rename_project(project) + + expect(subject).to receive(:perform_rename) + .with( + kind_of(Gitlab::Database::RenameReservedPathsMigration::V1::MigrationClasses::Project), + 'known-parent/the-path0', + 'known-parent/the-path' + ).and_call_original + + subject.revert_renames + + expect(project.reload.path).to eq('the-path') + expect(project.route.path).to eq('known-parent/the-path') + end + + it 'moves the repositories back to their original place' do + project.create_repository + subject.rename_project(project) + + expected_path = File.join(TestEnv.repos_path, 'known-parent', 'the-path.git') + + expect(subject).to receive(:move_project_folders) + .with( + kind_of(Gitlab::Database::RenameReservedPathsMigration::V1::MigrationClasses::Project), + 'known-parent/the-path0', + 'known-parent/the-path' + ).and_call_original + + subject.revert_renames + + expect(File.directory?(expected_path)).to be_truthy + end + + it "doesn't break when the project was renamed" do + subject.rename_project(project) + project.update_attributes!(path: 'renamed-afterwards') + + expect { subject.revert_renames }.not_to raise_error + end + end end diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb index 1d5e58855c1..7695b95dc57 100644 --- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb +++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb @@ -51,4 +51,26 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1, :truncate do subject.rename_root_paths('the-path') end end + + describe '#revert_renames' do + it 'renames namespaces' do + rename_namespaces = double + expect(described_class::RenameNamespaces) + .to receive(:new).with([], subject) + .and_return(rename_namespaces) + expect(rename_namespaces).to receive(:revert_renames) + + subject.revert_renames + end + + it 'renames projects' do + rename_projects = double + expect(described_class::RenameProjects) + .to receive(:new).with([], subject) + .and_return(rename_projects) + expect(rename_projects).to receive(:revert_renames) + + subject.revert_renames + end + end end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index ee25aeefa95..0cd458bf933 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -26,6 +26,10 @@ describe Gitlab::Git::Repository, seed_helper: true do end end + it 'returns UTF-8' do + expect(repository.root_ref.encoding).to eq(Encoding.find('UTF-8')) + end + context 'with gitaly enabled' do before do stub_gitaly @@ -123,6 +127,11 @@ describe Gitlab::Git::Repository, seed_helper: true do it 'has SeedRepo::Repo::BRANCHES.size elements' do expect(subject.size).to eq(SeedRepo::Repo::BRANCHES.size) end + + it 'returns UTF-8' do + expect(subject.first.encoding).to eq(Encoding.find('UTF-8')) + end + it { is_expected.to include("master") } it { is_expected.not_to include("branch-from-space") } @@ -158,10 +167,15 @@ describe Gitlab::Git::Repository, seed_helper: true do subject { repository.tag_names } it { is_expected.to be_kind_of Array } + it 'has SeedRepo::Repo::TAGS.size elements' do expect(subject.size).to eq(SeedRepo::Repo::TAGS.size) end + it 'returns UTF-8' do + expect(subject.first.encoding).to eq(Encoding.find('UTF-8')) + end + describe '#last' do subject { super().last } it { is_expected.to eq("v1.2.1") } @@ -1276,6 +1290,16 @@ describe Gitlab::Git::Repository, seed_helper: true do Gitlab::GitalyClient.clear_stubs! end + it 'returns a Branch with UTF-8 fields' do + branches = @repo.local_branches.to_a + expect(branches.size).to be > 0 + utf_8 = Encoding.find('utf-8') + branches.each do |branch| + expect(branch.name.encoding).to eq(utf_8) + expect(branch.target.encoding).to eq(utf_8) unless branch.target.nil? + end + end + it 'gets the branches from GitalyClient' do expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:local_branches) .and_return([]) diff --git a/spec/lib/gitlab/gitaly_client/ref_spec.rb b/spec/lib/gitlab/gitaly_client/ref_spec.rb index 42dba2ff874..8ad39a02b93 100644 --- a/spec/lib/gitlab/gitaly_client/ref_spec.rb +++ b/spec/lib/gitlab/gitaly_client/ref_spec.rb @@ -69,6 +69,15 @@ describe Gitlab::GitalyClient::Ref do client.local_branches(sort_by: 'updated_desc') end + it 'translates known mismatches on sort param values' do + expect_any_instance_of(Gitaly::Ref::Stub) + .to receive(:find_local_branches) + .with(gitaly_request_with_params(sort_by: :NAME), kind_of(Hash)) + .and_return([]) + + client.local_branches(sort_by: 'name_asc') + end + it 'raises an argument error if an invalid sort_by parameter is passed' do expect { client.local_branches(sort_by: 'invalid_sort') }.to raise_error(ArgumentError) end diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb index 090f9e70c50..dc7a0d80752 100644 --- a/spec/models/ability_spec.rb +++ b/spec/models/ability_spec.rb @@ -2,8 +2,8 @@ require 'spec_helper' describe Ability, lib: true do context 'using a nil subject' do - it 'is always empty' do - expect(Ability.allowed(nil, nil).to_set).to be_empty + it 'has no permissions' do + expect(Ability.policy_for(nil, nil)).to be_banned end end @@ -255,12 +255,15 @@ describe Ability, lib: true do describe '.project_disabled_features_rules' do let(:project) { create(:empty_project, :wiki_disabled) } - subject { described_class.allowed(project.owner, project) } + subject { described_class.policy_for(project.owner, project) } context 'wiki named abilities' do it 'disables wiki abilities if the project has no wiki' do expect(project).to receive(:has_external_wiki?).and_return(false) - expect(subject).not_to include(:read_wiki, :create_wiki, :update_wiki, :admin_wiki) + expect(subject).not_to be_allowed(:read_wiki) + expect(subject).not_to be_allowed(:create_wiki) + expect(subject).not_to be_allowed(:update_wiki) + expect(subject).not_to be_allowed(:admin_wiki) end end end diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb index 83494af24ba..329682a0771 100644 --- a/spec/models/ci/variable_spec.rb +++ b/spec/models/ci/variable_spec.rb @@ -3,14 +3,8 @@ require 'spec_helper' describe Ci::Variable, models: true do subject { build(:ci_variable) } - let(:secret_value) { 'secret' } - - it { is_expected.to validate_presence_of(:key) } + it { is_expected.to include_module(HasVariable) } it { is_expected.to validate_uniqueness_of(:key).scoped_to(:project_id) } - it { is_expected.to validate_length_of(:key).is_at_most(255) } - it { is_expected.to allow_value('foo').for(:key) } - it { is_expected.not_to allow_value('foo bar').for(:key) } - it { is_expected.not_to allow_value('foo/bar').for(:key) } describe '.unprotected' do subject { described_class.unprotected } @@ -33,36 +27,4 @@ describe Ci::Variable, models: true do end end end - - describe '#value' do - before do - subject.value = secret_value - end - - it 'stores the encrypted value' do - expect(subject.encrypted_value).not_to be_nil - end - - it 'stores an iv for value' do - expect(subject.encrypted_value_iv).not_to be_nil - end - - it 'stores a salt for value' do - expect(subject.encrypted_value_salt).not_to be_nil - end - - it 'fails to decrypt if iv is incorrect' do - subject.encrypted_value_iv = SecureRandom.hex - subject.instance_variable_set(:@value, nil) - expect { subject.value } - .to raise_error(OpenSSL::Cipher::CipherError, 'bad decrypt') - end - end - - describe '#to_runner_variable' do - it 'returns a hash for the runner' do - expect(subject.to_runner_variable) - .to eq(key: subject.key, value: subject.value, public: false) - end - end end diff --git a/spec/models/concerns/has_variable_spec.rb b/spec/models/concerns/has_variable_spec.rb new file mode 100644 index 00000000000..f4b24e6d1d9 --- /dev/null +++ b/spec/models/concerns/has_variable_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +describe HasVariable do + subject { build(:ci_variable) } + + it { is_expected.to validate_presence_of(:key) } + it { is_expected.to validate_length_of(:key).is_at_most(255) } + it { is_expected.to allow_value('foo').for(:key) } + it { is_expected.not_to allow_value('foo bar').for(:key) } + it { is_expected.not_to allow_value('foo/bar').for(:key) } + + describe '#value' do + before do + subject.value = 'secret' + end + + it 'stores the encrypted value' do + expect(subject.encrypted_value).not_to be_nil + end + + it 'stores an iv for value' do + expect(subject.encrypted_value_iv).not_to be_nil + end + + it 'stores a salt for value' do + expect(subject.encrypted_value_salt).not_to be_nil + end + + it 'fails to decrypt if iv is incorrect' do + subject.encrypted_value_iv = SecureRandom.hex + subject.instance_variable_set(:@value, nil) + expect { subject.value } + .to raise_error(OpenSSL::Cipher::CipherError, 'bad decrypt') + end + end + + describe '#to_runner_variable' do + it 'returns a hash for the runner' do + expect(subject.to_runner_variable) + .to eq(key: subject.key, value: subject.value, public: false) + end + end +end diff --git a/spec/models/concerns/sortable_spec.rb b/spec/models/concerns/sortable_spec.rb new file mode 100644 index 00000000000..d1e17c4f684 --- /dev/null +++ b/spec/models/concerns/sortable_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Sortable do + let(:relation) { Issue.all } + + describe '#where' do + it 'orders by id, descending' do + order_node = relation.where(iid: 1).order_values.first + expect(order_node).to be_a(Arel::Nodes::Descending) + expect(order_node.expr.name).to eq(:id) + end + end + + describe '#find_by' do + it 'does not order' do + expect(relation).to receive(:unscope).with(:order).and_call_original + + relation.find_by(iid: 1) + end + end +end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index e7c3acf19eb..d4f898f6d9f 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -323,6 +323,25 @@ describe Namespace, models: true do end end + describe '#users_with_descendants', :nested_groups do + let(:user_a) { create(:user) } + let(:user_b) { create(:user) } + + let(:group) { create(:group) } + let(:nested_group) { create(:group, parent: group) } + let(:deep_nested_group) { create(:group, parent: nested_group) } + + it 'returns member users on every nest level without duplication' do + group.add_developer(user_a) + nested_group.add_developer(user_b) + deep_nested_group.add_developer(user_a) + + expect(group.users_with_descendants).to contain_exactly(user_a, user_b) + expect(nested_group.users_with_descendants).to contain_exactly(user_a, user_b) + expect(deep_nested_group.users_with_descendants).to contain_exactly(user_a) + end + end + describe '#user_ids_for_project_authorizations' do it 'returns the user IDs for which to refresh authorizations' do expect(namespace.user_ids_for_project_authorizations) diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index cc22b8a4edc..5565fd2d391 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -284,15 +284,6 @@ describe Project, models: true do end end - describe 'default_scope' do - it 'excludes projects pending deletion from the results' do - project = create(:empty_project) - create(:empty_project, pending_delete: true) - - expect(Project.all).to eq [project] - end - end - describe 'project token' do it 'sets an random token if none provided' do project = FactoryGirl.create :empty_project, runners_token: '' @@ -1509,6 +1500,40 @@ describe Project, models: true do end end + describe 'project import state transitions' do + context 'state transition: [:started] => [:finished]' do + let(:housekeeping_service) { spy } + + before do + allow(Projects::HousekeepingService).to receive(:new) { housekeeping_service } + end + + it 'performs housekeeping when an import of a fresh project is completed' do + project = create(:project_empty_repo, :import_started, import_type: :github) + + project.import_finish + + expect(housekeeping_service).to have_received(:execute) + end + + it 'does not perform housekeeping when project repository does not exist' do + project = create(:empty_project, :import_started, import_type: :github) + + project.import_finish + + expect(housekeeping_service).not_to have_received(:execute) + end + + it 'does not perform housekeeping when project does not have a valid import type' do + project = create(:empty_project, :import_started, import_type: nil) + + project.import_finish + + expect(housekeeping_service).not_to have_received(:execute) + end + end + end + describe '#latest_successful_builds_for' do def create_pipeline(status = 'success') create(:ci_pipeline, project: project, diff --git a/spec/policies/base_policy_spec.rb b/spec/policies/base_policy_spec.rb index 02acdcb36df..e1963091a72 100644 --- a/spec/policies/base_policy_spec.rb +++ b/spec/policies/base_policy_spec.rb @@ -3,17 +3,17 @@ require 'spec_helper' describe BasePolicy, models: true do describe '.class_for' do it 'detects policy class based on the subject ancestors' do - expect(described_class.class_for(GenericCommitStatus.new)).to eq(CommitStatusPolicy) + expect(DeclarativePolicy.class_for(GenericCommitStatus.new)).to eq(CommitStatusPolicy) end it 'detects policy class for a presented subject' do presentee = Ci::BuildPresenter.new(Ci::Build.new) - expect(described_class.class_for(presentee)).to eq(Ci::BuildPolicy) + expect(DeclarativePolicy.class_for(presentee)).to eq(Ci::BuildPolicy) end it 'uses GlobalPolicy when :global is given' do - expect(described_class.class_for(:global)).to eq(GlobalPolicy) + expect(DeclarativePolicy.class_for(:global)).to eq(GlobalPolicy) end end end diff --git a/spec/policies/ci/build_policy_spec.rb b/spec/policies/ci/build_policy_spec.rb index 48a139d4b83..ace95ac7067 100644 --- a/spec/policies/ci/build_policy_spec.rb +++ b/spec/policies/ci/build_policy_spec.rb @@ -5,8 +5,8 @@ describe Ci::BuildPolicy, :models do let(:build) { create(:ci_build, pipeline: pipeline) } let(:pipeline) { create(:ci_empty_pipeline, project: project) } - let(:policies) do - described_class.abilities(user, build).to_set + let(:policy) do + described_class.new(user, build) end shared_context 'public pipelines disabled' do @@ -21,7 +21,7 @@ describe Ci::BuildPolicy, :models do context 'when public builds are enabled' do it 'does not include ability to read build' do - expect(policies).not_to include :read_build + expect(policy).not_to be_allowed :read_build end end @@ -29,7 +29,7 @@ describe Ci::BuildPolicy, :models do include_context 'public pipelines disabled' it 'does not include ability to read build' do - expect(policies).not_to include :read_build + expect(policy).not_to be_allowed :read_build end end end @@ -39,7 +39,7 @@ describe Ci::BuildPolicy, :models do context 'when public builds are enabled' do it 'includes ability to read build' do - expect(policies).to include :read_build + expect(policy).to be_allowed :read_build end end @@ -47,7 +47,7 @@ describe Ci::BuildPolicy, :models do include_context 'public pipelines disabled' it 'does not include ability to read build' do - expect(policies).not_to include :read_build + expect(policy).not_to be_allowed :read_build end end end @@ -62,7 +62,7 @@ describe Ci::BuildPolicy, :models do context 'when public builds are enabled' do it 'includes ability to read build' do - expect(policies).to include :read_build + expect(policy).to be_allowed :read_build end end @@ -70,7 +70,7 @@ describe Ci::BuildPolicy, :models do include_context 'public pipelines disabled' it 'does not include ability to read build' do - expect(policies).not_to include :read_build + expect(policy).not_to be_allowed :read_build end end end @@ -82,7 +82,7 @@ describe Ci::BuildPolicy, :models do context 'when public builds are enabled' do it 'includes ability to read build' do - expect(policies).to include :read_build + expect(policy).to be_allowed :read_build end end @@ -90,7 +90,7 @@ describe Ci::BuildPolicy, :models do include_context 'public pipelines disabled' it 'does not include ability to read build' do - expect(policies).to include :read_build + expect(policy).to be_allowed :read_build end end end @@ -115,7 +115,7 @@ describe Ci::BuildPolicy, :models do end it 'does not include ability to update build' do - expect(policies).not_to include :update_build + expect(policy).to be_disallowed :update_build end end @@ -125,7 +125,7 @@ describe Ci::BuildPolicy, :models do end it 'includes ability to update build' do - expect(policies).to include :update_build + expect(policy).to be_allowed :update_build end end end @@ -135,7 +135,7 @@ describe Ci::BuildPolicy, :models do let(:build) { create(:ci_build, :manual, pipeline: pipeline) } it 'includes ability to update build' do - expect(policies).to include :update_build + expect(policy).to be_allowed :update_build end end @@ -143,7 +143,7 @@ describe Ci::BuildPolicy, :models do let(:build) { create(:ci_build, pipeline: pipeline) } it 'includes ability to update build' do - expect(policies).to include :update_build + expect(policy).to be_allowed :update_build end end end diff --git a/spec/policies/ci/trigger_policy_spec.rb b/spec/policies/ci/trigger_policy_spec.rb index 63ad5eb7322..ed4010e723b 100644 --- a/spec/policies/ci/trigger_policy_spec.rb +++ b/spec/policies/ci/trigger_policy_spec.rb @@ -6,36 +6,36 @@ describe Ci::TriggerPolicy, :models do let(:trigger) { create(:ci_trigger, project: project, owner: owner) } let(:policies) do - described_class.abilities(user, trigger).to_set + described_class.new(user, trigger) end shared_examples 'allows to admin and manage trigger' do it 'does include ability to admin trigger' do - expect(policies).to include :admin_trigger + expect(policies).to be_allowed :admin_trigger end it 'does include ability to manage trigger' do - expect(policies).to include :manage_trigger + expect(policies).to be_allowed :manage_trigger end end shared_examples 'allows to manage trigger' do it 'does not include ability to admin trigger' do - expect(policies).not_to include :admin_trigger + expect(policies).not_to be_allowed :admin_trigger end it 'does include ability to manage trigger' do - expect(policies).to include :manage_trigger + expect(policies).to be_allowed :manage_trigger end end shared_examples 'disallows to admin and manage trigger' do it 'does not include ability to admin trigger' do - expect(policies).not_to include :admin_trigger + expect(policies).not_to be_allowed :admin_trigger end it 'does not include ability to manage trigger' do - expect(policies).not_to include :manage_trigger + expect(policies).not_to be_allowed :manage_trigger end end diff --git a/spec/policies/deploy_key_policy_spec.rb b/spec/policies/deploy_key_policy_spec.rb index 28e10f0bfe2..f15f4a11f02 100644 --- a/spec/policies/deploy_key_policy_spec.rb +++ b/spec/policies/deploy_key_policy_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe DeployKeyPolicy, models: true do - subject { described_class.abilities(current_user, deploy_key).to_set } + subject { described_class.new(current_user, deploy_key) } describe 'updating a deploy_key' do context 'when a regular user' do @@ -16,7 +16,7 @@ describe DeployKeyPolicy, models: true do project.deploy_keys << deploy_key end - it { is_expected.to include(:update_deploy_key) } + it { is_expected.to be_allowed(:update_deploy_key) } end context 'tries to update private deploy key attached to other project' do @@ -27,13 +27,13 @@ describe DeployKeyPolicy, models: true do other_project.deploy_keys << deploy_key end - it { is_expected.not_to include(:update_deploy_key) } + it { is_expected.to be_disallowed(:update_deploy_key) } end context 'tries to update public deploy key' do let(:deploy_key) { create(:another_deploy_key, public: true) } - it { is_expected.not_to include(:update_deploy_key) } + it { is_expected.to be_disallowed(:update_deploy_key) } end end @@ -43,13 +43,13 @@ describe DeployKeyPolicy, models: true do context ' tries to update private deploy key' do let(:deploy_key) { create(:deploy_key, public: false) } - it { is_expected.to include(:update_deploy_key) } + it { is_expected.to be_allowed(:update_deploy_key) } end context 'when an admin user tries to update public deploy key' do let(:deploy_key) { create(:another_deploy_key, public: true) } - it { is_expected.to include(:update_deploy_key) } + it { is_expected.to be_allowed(:update_deploy_key) } end end end diff --git a/spec/policies/environment_policy_spec.rb b/spec/policies/environment_policy_spec.rb index 650432520bb..035e20c7452 100644 --- a/spec/policies/environment_policy_spec.rb +++ b/spec/policies/environment_policy_spec.rb @@ -8,8 +8,8 @@ describe EnvironmentPolicy do create(:environment, :with_review_app, project: project) end - let(:policies) do - described_class.abilities(user, environment).to_set + let(:policy) do + described_class.new(user, environment) end describe '#rules' do @@ -17,7 +17,7 @@ describe EnvironmentPolicy do let(:project) { create(:project, :private) } it 'does not include ability to stop environment' do - expect(policies).not_to include :stop_environment + expect(policy).to be_disallowed :stop_environment end end @@ -25,7 +25,7 @@ describe EnvironmentPolicy do let(:project) { create(:project, :public) } it 'does not include ability to stop environment' do - expect(policies).not_to include :stop_environment + expect(policy).to be_disallowed :stop_environment end end @@ -38,7 +38,7 @@ describe EnvironmentPolicy do context 'when team member has ability to stop environment' do it 'does includes ability to stop environment' do - expect(policies).to include :stop_environment + expect(policy).to be_allowed :stop_environment end end @@ -49,7 +49,7 @@ describe EnvironmentPolicy do end it 'does not include ability to stop environment' do - expect(policies).not_to include :stop_environment + expect(policy).to be_disallowed :stop_environment end end end diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb index a8331ceb5ff..06db0ea56e3 100644 --- a/spec/policies/group_policy_spec.rb +++ b/spec/policies/group_policy_spec.rb @@ -36,16 +36,24 @@ describe GroupPolicy, models: true do group.add_owner(owner) end - subject { described_class.abilities(current_user, group).to_set } + subject { described_class.new(current_user, group) } + + def expect_allowed(*permissions) + permissions.each { |p| is_expected.to be_allowed(p) } + end + + def expect_disallowed(*permissions) + permissions.each { |p| is_expected.not_to be_allowed(p) } + end context 'with no user' do let(:current_user) { nil } it do - is_expected.to include(:read_group) - is_expected.not_to include(*reporter_permissions) - is_expected.not_to include(*master_permissions) - is_expected.not_to include(*owner_permissions) + expect_allowed(:read_group) + expect_disallowed(*reporter_permissions) + expect_disallowed(*master_permissions) + expect_disallowed(*owner_permissions) end end @@ -53,10 +61,10 @@ describe GroupPolicy, models: true do let(:current_user) { guest } it do - is_expected.to include(:read_group) - is_expected.not_to include(*reporter_permissions) - is_expected.not_to include(*master_permissions) - is_expected.not_to include(*owner_permissions) + expect_allowed(:read_group) + expect_disallowed(*reporter_permissions) + expect_disallowed(*master_permissions) + expect_disallowed(*owner_permissions) end end @@ -64,10 +72,10 @@ describe GroupPolicy, models: true do let(:current_user) { reporter } it do - is_expected.to include(:read_group) - is_expected.to include(*reporter_permissions) - is_expected.not_to include(*master_permissions) - is_expected.not_to include(*owner_permissions) + expect_allowed(:read_group) + expect_allowed(*reporter_permissions) + expect_disallowed(*master_permissions) + expect_disallowed(*owner_permissions) end end @@ -75,10 +83,10 @@ describe GroupPolicy, models: true do let(:current_user) { developer } it do - is_expected.to include(:read_group) - is_expected.to include(*reporter_permissions) - is_expected.not_to include(*master_permissions) - is_expected.not_to include(*owner_permissions) + expect_allowed(:read_group) + expect_allowed(*reporter_permissions) + expect_disallowed(*master_permissions) + expect_disallowed(*owner_permissions) end end @@ -86,10 +94,10 @@ describe GroupPolicy, models: true do let(:current_user) { master } it do - is_expected.to include(:read_group) - is_expected.to include(*reporter_permissions) - is_expected.to include(*master_permissions) - is_expected.not_to include(*owner_permissions) + expect_allowed(:read_group) + expect_allowed(*reporter_permissions) + expect_allowed(*master_permissions) + expect_disallowed(*owner_permissions) end end @@ -97,10 +105,10 @@ describe GroupPolicy, models: true do let(:current_user) { owner } it do - is_expected.to include(:read_group) - is_expected.to include(*reporter_permissions) - is_expected.to include(*master_permissions) - is_expected.to include(*owner_permissions) + expect_allowed(:read_group) + expect_allowed(*reporter_permissions) + expect_allowed(*master_permissions) + expect_allowed(*owner_permissions) end end @@ -108,10 +116,10 @@ describe GroupPolicy, models: true do let(:current_user) { admin } it do - is_expected.to include(:read_group) - is_expected.to include(*reporter_permissions) - is_expected.to include(*master_permissions) - is_expected.to include(*owner_permissions) + expect_allowed(:read_group) + expect_allowed(*reporter_permissions) + expect_allowed(*master_permissions) + expect_allowed(*owner_permissions) end end @@ -130,16 +138,16 @@ describe GroupPolicy, models: true do nested_group.add_owner(owner) end - subject { described_class.abilities(current_user, nested_group).to_set } + subject { described_class.new(current_user, nested_group) } context 'with no user' do let(:current_user) { nil } it do - is_expected.not_to include(:read_group) - is_expected.not_to include(*reporter_permissions) - is_expected.not_to include(*master_permissions) - is_expected.not_to include(*owner_permissions) + expect_disallowed(:read_group) + expect_disallowed(*reporter_permissions) + expect_disallowed(*master_permissions) + expect_disallowed(*owner_permissions) end end @@ -147,10 +155,10 @@ describe GroupPolicy, models: true do let(:current_user) { guest } it do - is_expected.to include(:read_group) - is_expected.not_to include(*reporter_permissions) - is_expected.not_to include(*master_permissions) - is_expected.not_to include(*owner_permissions) + expect_allowed(:read_group) + expect_disallowed(*reporter_permissions) + expect_disallowed(*master_permissions) + expect_disallowed(*owner_permissions) end end @@ -158,10 +166,10 @@ describe GroupPolicy, models: true do let(:current_user) { reporter } it do - is_expected.to include(:read_group) - is_expected.to include(*reporter_permissions) - is_expected.not_to include(*master_permissions) - is_expected.not_to include(*owner_permissions) + expect_allowed(:read_group) + expect_allowed(*reporter_permissions) + expect_disallowed(*master_permissions) + expect_disallowed(*owner_permissions) end end @@ -169,10 +177,10 @@ describe GroupPolicy, models: true do let(:current_user) { developer } it do - is_expected.to include(:read_group) - is_expected.to include(*reporter_permissions) - is_expected.not_to include(*master_permissions) - is_expected.not_to include(*owner_permissions) + expect_allowed(:read_group) + expect_allowed(*reporter_permissions) + expect_disallowed(*master_permissions) + expect_disallowed(*owner_permissions) end end @@ -180,10 +188,10 @@ describe GroupPolicy, models: true do let(:current_user) { master } it do - is_expected.to include(:read_group) - is_expected.to include(*reporter_permissions) - is_expected.to include(*master_permissions) - is_expected.not_to include(*owner_permissions) + expect_allowed(:read_group) + expect_allowed(*reporter_permissions) + expect_allowed(*master_permissions) + expect_disallowed(*owner_permissions) end end @@ -191,10 +199,10 @@ describe GroupPolicy, models: true do let(:current_user) { owner } it do - is_expected.to include(:read_group) - is_expected.to include(*reporter_permissions) - is_expected.to include(*master_permissions) - is_expected.to include(*owner_permissions) + expect_allowed(:read_group) + expect_allowed(*reporter_permissions) + expect_allowed(*master_permissions) + expect_allowed(*owner_permissions) end end end diff --git a/spec/policies/issue_policy_spec.rb b/spec/policies/issue_policy_spec.rb index 4a07c864428..c978cbd6185 100644 --- a/spec/policies/issue_policy_spec.rb +++ b/spec/policies/issue_policy_spec.rb @@ -9,7 +9,7 @@ describe IssuePolicy, models: true do let(:reporter_from_group_link) { create(:user) } def permissions(user, issue) - described_class.abilities(user, issue).to_set + described_class.new(user, issue) end context 'a private project' do @@ -30,42 +30,42 @@ describe IssuePolicy, models: true do end it 'does not allow non-members to read issues' do - expect(permissions(non_member, issue)).not_to include(:read_issue, :update_issue, :admin_issue) - expect(permissions(non_member, issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(non_member, issue)).to be_disallowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(non_member, issue_no_assignee)).to be_disallowed(:read_issue, :update_issue, :admin_issue) end it 'allows guests to read issues' do - expect(permissions(guest, issue)).to include(:read_issue) - expect(permissions(guest, issue)).not_to include(:update_issue, :admin_issue) + expect(permissions(guest, issue)).to be_allowed(:read_issue) + expect(permissions(guest, issue)).to be_disallowed(:update_issue, :admin_issue) - expect(permissions(guest, issue_no_assignee)).to include(:read_issue) - expect(permissions(guest, issue_no_assignee)).not_to include(:update_issue, :admin_issue) + expect(permissions(guest, issue_no_assignee)).to be_allowed(:read_issue) + expect(permissions(guest, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue) end it 'allows reporters to read, update, and admin issues' do - expect(permissions(reporter, issue)).to include(:read_issue, :update_issue, :admin_issue) - expect(permissions(reporter, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter, issue)).to be_allowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter, issue_no_assignee)).to be_allowed(:read_issue, :update_issue, :admin_issue) end it 'allows reporters from group links to read, update, and admin issues' do - expect(permissions(reporter_from_group_link, issue)).to include(:read_issue, :update_issue, :admin_issue) - expect(permissions(reporter_from_group_link, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter_from_group_link, issue)).to be_allowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter_from_group_link, issue_no_assignee)).to be_allowed(:read_issue, :update_issue, :admin_issue) end it 'allows issue authors to read and update their issues' do - expect(permissions(author, issue)).to include(:read_issue, :update_issue) - expect(permissions(author, issue)).not_to include(:admin_issue) + expect(permissions(author, issue)).to be_allowed(:read_issue, :update_issue) + expect(permissions(author, issue)).to be_disallowed(:admin_issue) - expect(permissions(author, issue_no_assignee)).to include(:read_issue) - expect(permissions(author, issue_no_assignee)).not_to include(:update_issue, :admin_issue) + expect(permissions(author, issue_no_assignee)).to be_allowed(:read_issue) + expect(permissions(author, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue) end it 'allows issue assignees to read and update their issues' do - expect(permissions(assignee, issue)).to include(:read_issue, :update_issue) - expect(permissions(assignee, issue)).not_to include(:admin_issue) + expect(permissions(assignee, issue)).to be_allowed(:read_issue, :update_issue) + expect(permissions(assignee, issue)).to be_disallowed(:admin_issue) - expect(permissions(assignee, issue_no_assignee)).to include(:read_issue) - expect(permissions(assignee, issue_no_assignee)).not_to include(:update_issue, :admin_issue) + expect(permissions(assignee, issue_no_assignee)).to be_allowed(:read_issue) + expect(permissions(assignee, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue) end context 'with confidential issues' do @@ -73,37 +73,37 @@ describe IssuePolicy, models: true do let(:confidential_issue_no_assignee) { create(:issue, :confidential, project: project) } it 'does not allow non-members to read confidential issues' do - expect(permissions(non_member, confidential_issue)).not_to include(:read_issue, :update_issue, :admin_issue) - expect(permissions(non_member, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(non_member, confidential_issue)).to be_disallowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(non_member, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :update_issue, :admin_issue) end it 'does not allow guests to read confidential issues' do - expect(permissions(guest, confidential_issue)).not_to include(:read_issue, :update_issue, :admin_issue) - expect(permissions(guest, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(guest, confidential_issue)).to be_disallowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(guest, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :update_issue, :admin_issue) end it 'allows reporters to read, update, and admin confidential issues' do - expect(permissions(reporter, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue) - expect(permissions(reporter, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter, confidential_issue)).to be_allowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter, confidential_issue_no_assignee)).to be_allowed(:read_issue, :update_issue, :admin_issue) end it 'allows reporters from group links to read, update, and admin confidential issues' do - expect(permissions(reporter_from_group_link, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue) - expect(permissions(reporter_from_group_link, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter_from_group_link, confidential_issue)).to be_allowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter_from_group_link, confidential_issue_no_assignee)).to be_allowed(:read_issue, :update_issue, :admin_issue) end it 'allows issue authors to read and update their confidential issues' do - expect(permissions(author, confidential_issue)).to include(:read_issue, :update_issue) - expect(permissions(author, confidential_issue)).not_to include(:admin_issue) + expect(permissions(author, confidential_issue)).to be_allowed(:read_issue, :update_issue) + expect(permissions(author, confidential_issue)).to be_disallowed(:admin_issue) - expect(permissions(author, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(author, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :update_issue, :admin_issue) end it 'allows issue assignees to read and update their confidential issues' do - expect(permissions(assignee, confidential_issue)).to include(:read_issue, :update_issue) - expect(permissions(assignee, confidential_issue)).not_to include(:admin_issue) + expect(permissions(assignee, confidential_issue)).to be_allowed(:read_issue, :update_issue) + expect(permissions(assignee, confidential_issue)).to be_disallowed(:admin_issue) - expect(permissions(assignee, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(assignee, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :update_issue, :admin_issue) end end end @@ -123,37 +123,37 @@ describe IssuePolicy, models: true do end it 'allows guests to read issues' do - expect(permissions(guest, issue)).to include(:read_issue) - expect(permissions(guest, issue)).not_to include(:update_issue, :admin_issue) + expect(permissions(guest, issue)).to be_allowed(:read_issue) + expect(permissions(guest, issue)).to be_disallowed(:update_issue, :admin_issue) - expect(permissions(guest, issue_no_assignee)).to include(:read_issue) - expect(permissions(guest, issue_no_assignee)).not_to include(:update_issue, :admin_issue) + expect(permissions(guest, issue_no_assignee)).to be_allowed(:read_issue) + expect(permissions(guest, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue) end it 'allows reporters to read, update, and admin issues' do - expect(permissions(reporter, issue)).to include(:read_issue, :update_issue, :admin_issue) - expect(permissions(reporter, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter, issue)).to be_allowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter, issue_no_assignee)).to be_allowed(:read_issue, :update_issue, :admin_issue) end it 'allows reporters from group links to read, update, and admin issues' do - expect(permissions(reporter_from_group_link, issue)).to include(:read_issue, :update_issue, :admin_issue) - expect(permissions(reporter_from_group_link, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter_from_group_link, issue)).to be_allowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter_from_group_link, issue_no_assignee)).to be_allowed(:read_issue, :update_issue, :admin_issue) end it 'allows issue authors to read and update their issues' do - expect(permissions(author, issue)).to include(:read_issue, :update_issue) - expect(permissions(author, issue)).not_to include(:admin_issue) + expect(permissions(author, issue)).to be_allowed(:read_issue, :update_issue) + expect(permissions(author, issue)).to be_disallowed(:admin_issue) - expect(permissions(author, issue_no_assignee)).to include(:read_issue) - expect(permissions(author, issue_no_assignee)).not_to include(:update_issue, :admin_issue) + expect(permissions(author, issue_no_assignee)).to be_allowed(:read_issue) + expect(permissions(author, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue) end it 'allows issue assignees to read and update their issues' do - expect(permissions(assignee, issue)).to include(:read_issue, :update_issue) - expect(permissions(assignee, issue)).not_to include(:admin_issue) + expect(permissions(assignee, issue)).to be_allowed(:read_issue, :update_issue) + expect(permissions(assignee, issue)).to be_disallowed(:admin_issue) - expect(permissions(assignee, issue_no_assignee)).to include(:read_issue) - expect(permissions(assignee, issue_no_assignee)).not_to include(:update_issue, :admin_issue) + expect(permissions(assignee, issue_no_assignee)).to be_allowed(:read_issue) + expect(permissions(assignee, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue) end context 'with confidential issues' do @@ -161,32 +161,32 @@ describe IssuePolicy, models: true do let(:confidential_issue_no_assignee) { create(:issue, :confidential, project: project) } it 'does not allow guests to read confidential issues' do - expect(permissions(guest, confidential_issue)).not_to include(:read_issue, :update_issue, :admin_issue) - expect(permissions(guest, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(guest, confidential_issue)).to be_disallowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(guest, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :update_issue, :admin_issue) end it 'allows reporters to read, update, and admin confidential issues' do - expect(permissions(reporter, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue) - expect(permissions(reporter, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter, confidential_issue)).to be_allowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter, confidential_issue_no_assignee)).to be_allowed(:read_issue, :update_issue, :admin_issue) end it 'allows reporter from group links to read, update, and admin confidential issues' do - expect(permissions(reporter_from_group_link, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue) - expect(permissions(reporter_from_group_link, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter_from_group_link, confidential_issue)).to be_allowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter_from_group_link, confidential_issue_no_assignee)).to be_allowed(:read_issue, :update_issue, :admin_issue) end it 'allows issue authors to read and update their confidential issues' do - expect(permissions(author, confidential_issue)).to include(:read_issue, :update_issue) - expect(permissions(author, confidential_issue)).not_to include(:admin_issue) + expect(permissions(author, confidential_issue)).to be_allowed(:read_issue, :update_issue) + expect(permissions(author, confidential_issue)).to be_disallowed(:admin_issue) - expect(permissions(author, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(author, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :update_issue, :admin_issue) end it 'allows issue assignees to read and update their confidential issues' do - expect(permissions(assignee, confidential_issue)).to include(:read_issue, :update_issue) - expect(permissions(assignee, confidential_issue)).not_to include(:admin_issue) + expect(permissions(assignee, confidential_issue)).to be_allowed(:read_issue, :update_issue) + expect(permissions(assignee, confidential_issue)).to be_disallowed(:admin_issue) - expect(permissions(assignee, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(assignee, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :update_issue, :admin_issue) end end end diff --git a/spec/policies/personal_snippet_policy_spec.rb b/spec/policies/personal_snippet_policy_spec.rb index 58aa1145c9e..4d6350fc653 100644 --- a/spec/policies/personal_snippet_policy_spec.rb +++ b/spec/policies/personal_snippet_policy_spec.rb @@ -14,7 +14,7 @@ describe PersonalSnippetPolicy, models: true do end def permissions(user) - described_class.abilities(user, snippet).to_set + described_class.new(user, snippet) end context 'public snippet' do @@ -24,9 +24,9 @@ describe PersonalSnippetPolicy, models: true do subject { permissions(nil) } it do - is_expected.to include(:read_personal_snippet) - is_expected.not_to include(:comment_personal_snippet) - is_expected.not_to include(*author_permissions) + is_expected.to be_allowed(:read_personal_snippet) + is_expected.to be_disallowed(:comment_personal_snippet) + is_expected.to be_disallowed(*author_permissions) end end @@ -34,9 +34,9 @@ describe PersonalSnippetPolicy, models: true do subject { permissions(regular_user) } it do - is_expected.to include(:read_personal_snippet) - is_expected.to include(:comment_personal_snippet) - is_expected.not_to include(*author_permissions) + is_expected.to be_allowed(:read_personal_snippet) + is_expected.to be_allowed(:comment_personal_snippet) + is_expected.to be_disallowed(*author_permissions) end end @@ -44,9 +44,9 @@ describe PersonalSnippetPolicy, models: true do subject { permissions(snippet.author) } it do - is_expected.to include(:read_personal_snippet) - is_expected.to include(:comment_personal_snippet) - is_expected.to include(*author_permissions) + is_expected.to be_allowed(:read_personal_snippet) + is_expected.to be_allowed(:comment_personal_snippet) + is_expected.to be_allowed(*author_permissions) end end end @@ -58,9 +58,9 @@ describe PersonalSnippetPolicy, models: true do subject { permissions(nil) } it do - is_expected.not_to include(:read_personal_snippet) - is_expected.not_to include(:comment_personal_snippet) - is_expected.not_to include(*author_permissions) + is_expected.to be_disallowed(:read_personal_snippet) + is_expected.to be_disallowed(:comment_personal_snippet) + is_expected.to be_disallowed(*author_permissions) end end @@ -68,9 +68,9 @@ describe PersonalSnippetPolicy, models: true do subject { permissions(regular_user) } it do - is_expected.to include(:read_personal_snippet) - is_expected.to include(:comment_personal_snippet) - is_expected.not_to include(*author_permissions) + is_expected.to be_allowed(:read_personal_snippet) + is_expected.to be_allowed(:comment_personal_snippet) + is_expected.to be_disallowed(*author_permissions) end end @@ -78,9 +78,9 @@ describe PersonalSnippetPolicy, models: true do subject { permissions(external_user) } it do - is_expected.not_to include(:read_personal_snippet) - is_expected.not_to include(:comment_personal_snippet) - is_expected.not_to include(*author_permissions) + is_expected.to be_disallowed(:read_personal_snippet) + is_expected.to be_disallowed(:comment_personal_snippet) + is_expected.to be_disallowed(*author_permissions) end end @@ -88,9 +88,9 @@ describe PersonalSnippetPolicy, models: true do subject { permissions(snippet.author) } it do - is_expected.to include(:read_personal_snippet) - is_expected.to include(:comment_personal_snippet) - is_expected.to include(*author_permissions) + is_expected.to be_allowed(:read_personal_snippet) + is_expected.to be_allowed(:comment_personal_snippet) + is_expected.to be_allowed(*author_permissions) end end end @@ -102,9 +102,9 @@ describe PersonalSnippetPolicy, models: true do subject { permissions(nil) } it do - is_expected.not_to include(:read_personal_snippet) - is_expected.not_to include(:comment_personal_snippet) - is_expected.not_to include(*author_permissions) + is_expected.to be_disallowed(:read_personal_snippet) + is_expected.to be_disallowed(:comment_personal_snippet) + is_expected.to be_disallowed(*author_permissions) end end @@ -112,9 +112,9 @@ describe PersonalSnippetPolicy, models: true do subject { permissions(regular_user) } it do - is_expected.not_to include(:read_personal_snippet) - is_expected.not_to include(:comment_personal_snippet) - is_expected.not_to include(*author_permissions) + is_expected.to be_disallowed(:read_personal_snippet) + is_expected.to be_disallowed(:comment_personal_snippet) + is_expected.to be_disallowed(*author_permissions) end end @@ -122,9 +122,9 @@ describe PersonalSnippetPolicy, models: true do subject { permissions(external_user) } it do - is_expected.not_to include(:read_personal_snippet) - is_expected.not_to include(:comment_personal_snippet) - is_expected.not_to include(*author_permissions) + is_expected.to be_disallowed(:read_personal_snippet) + is_expected.to be_disallowed(:comment_personal_snippet) + is_expected.to be_disallowed(*author_permissions) end end @@ -132,9 +132,9 @@ describe PersonalSnippetPolicy, models: true do subject { permissions(snippet.author) } it do - is_expected.to include(:read_personal_snippet) - is_expected.to include(:comment_personal_snippet) - is_expected.to include(*author_permissions) + is_expected.to be_allowed(:read_personal_snippet) + is_expected.to be_allowed(:comment_personal_snippet) + is_expected.to be_allowed(*author_permissions) end end end diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index d70e15f006b..ca435dd0218 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -73,37 +73,45 @@ describe ProjectPolicy, models: true do project.team << [reporter, :reporter] end + def expect_allowed(*permissions) + permissions.each { |p| is_expected.to be_allowed(p) } + end + + def expect_disallowed(*permissions) + permissions.each { |p| is_expected.not_to be_allowed(p) } + end + it 'does not include the read_issue permission when the issue author is not a member of the private project' do project = create(:empty_project, :private) issue = create(:issue, project: project) user = issue.author - expect(project.team.member?(issue.author)).to eq(false) + expect(project.team.member?(issue.author)).to be false - expect(BasePolicy.class_for(project).abilities(user, project).can_set) - .not_to include(:read_issue) - - expect(Ability.allowed?(user, :read_issue, project)).to be_falsy + expect(Ability).not_to be_allowed(user, :read_issue, project) end - it 'does not include the wiki permissions when the feature is disabled' do - project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::DISABLED) - wiki_permissions = [:read_wiki, :create_wiki, :update_wiki, :admin_wiki, :download_wiki_code] + context 'when the feature is disabled' do + subject { described_class.new(owner, project) } - permissions = described_class.abilities(owner, project).to_set + before do + project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::DISABLED) + end - expect(permissions).not_to include(*wiki_permissions) + it 'does not include the wiki permissions' do + expect_disallowed :read_wiki, :create_wiki, :update_wiki, :admin_wiki, :download_wiki_code + end end context 'abilities for non-public projects' do let(:project) { create(:empty_project, namespace: owner.namespace) } - subject { described_class.abilities(current_user, project).to_set } + subject { described_class.new(current_user, project) } context 'with no user' do let(:current_user) { nil } - it { is_expected.to be_empty } + it { is_expected.to be_banned } end context 'guests' do @@ -114,18 +122,18 @@ describe ProjectPolicy, models: true do end it do - is_expected.to include(*guest_permissions) - is_expected.not_to include(*reporter_public_build_permissions) - is_expected.not_to include(*team_member_reporter_permissions) - is_expected.not_to include(*developer_permissions) - is_expected.not_to include(*master_permissions) - is_expected.not_to include(*owner_permissions) + expect_allowed(*guest_permissions) + expect_disallowed(*reporter_public_build_permissions) + expect_disallowed(*team_member_reporter_permissions) + expect_disallowed(*developer_permissions) + expect_disallowed(*master_permissions) + expect_disallowed(*owner_permissions) end context 'public builds enabled' do it do - is_expected.to include(*guest_permissions) - is_expected.to include(:read_build, :read_pipeline) + expect_allowed(*guest_permissions) + expect_allowed(:read_build, :read_pipeline) end end @@ -135,8 +143,8 @@ describe ProjectPolicy, models: true do end it do - is_expected.to include(*guest_permissions) - is_expected.not_to include(:read_build, :read_pipeline) + expect_allowed(*guest_permissions) + expect_disallowed(:read_build, :read_pipeline) end end @@ -147,8 +155,8 @@ describe ProjectPolicy, models: true do end it do - is_expected.not_to include(:read_build) - is_expected.to include(:read_pipeline) + expect_disallowed(:read_build) + expect_allowed(:read_pipeline) end end end @@ -157,12 +165,13 @@ describe ProjectPolicy, models: true do let(:current_user) { reporter } it do - is_expected.to include(*guest_permissions) - is_expected.to include(*reporter_permissions) - is_expected.to include(*team_member_reporter_permissions) - is_expected.not_to include(*developer_permissions) - is_expected.not_to include(*master_permissions) - is_expected.not_to include(*owner_permissions) + expect_allowed(*guest_permissions) + expect_allowed(*reporter_permissions) + expect_allowed(*reporter_permissions) + expect_allowed(*team_member_reporter_permissions) + expect_disallowed(*developer_permissions) + expect_disallowed(*master_permissions) + expect_disallowed(*owner_permissions) end end @@ -170,12 +179,12 @@ describe ProjectPolicy, models: true do let(:current_user) { dev } it do - is_expected.to include(*guest_permissions) - is_expected.to include(*reporter_permissions) - is_expected.to include(*team_member_reporter_permissions) - is_expected.to include(*developer_permissions) - is_expected.not_to include(*master_permissions) - is_expected.not_to include(*owner_permissions) + expect_allowed(*guest_permissions) + expect_allowed(*reporter_permissions) + expect_allowed(*team_member_reporter_permissions) + expect_allowed(*developer_permissions) + expect_disallowed(*master_permissions) + expect_disallowed(*owner_permissions) end end @@ -183,12 +192,12 @@ describe ProjectPolicy, models: true do let(:current_user) { master } it do - is_expected.to include(*guest_permissions) - is_expected.to include(*reporter_permissions) - is_expected.to include(*team_member_reporter_permissions) - is_expected.to include(*developer_permissions) - is_expected.to include(*master_permissions) - is_expected.not_to include(*owner_permissions) + expect_allowed(*guest_permissions) + expect_allowed(*reporter_permissions) + expect_allowed(*team_member_reporter_permissions) + expect_allowed(*developer_permissions) + expect_allowed(*master_permissions) + expect_disallowed(*owner_permissions) end end @@ -196,12 +205,12 @@ describe ProjectPolicy, models: true do let(:current_user) { owner } it do - is_expected.to include(*guest_permissions) - is_expected.to include(*reporter_permissions) - is_expected.to include(*team_member_reporter_permissions) - is_expected.to include(*developer_permissions) - is_expected.to include(*master_permissions) - is_expected.to include(*owner_permissions) + expect_allowed(*guest_permissions) + expect_allowed(*reporter_permissions) + expect_allowed(*team_member_reporter_permissions) + expect_allowed(*developer_permissions) + expect_allowed(*master_permissions) + expect_allowed(*owner_permissions) end end @@ -209,12 +218,12 @@ describe ProjectPolicy, models: true do let(:current_user) { admin } it do - is_expected.to include(*guest_permissions) - is_expected.to include(*reporter_permissions) - is_expected.not_to include(*team_member_reporter_permissions) - is_expected.to include(*developer_permissions) - is_expected.to include(*master_permissions) - is_expected.to include(*owner_permissions) + expect_allowed(*guest_permissions) + expect_allowed(*reporter_permissions) + expect_disallowed(*team_member_reporter_permissions) + expect_allowed(*developer_permissions) + expect_allowed(*master_permissions) + expect_allowed(*owner_permissions) end end end diff --git a/spec/policies/project_snippet_policy_spec.rb b/spec/policies/project_snippet_policy_spec.rb index d2b2528c57a..2799f03fb9b 100644 --- a/spec/policies/project_snippet_policy_spec.rb +++ b/spec/policies/project_snippet_policy_spec.rb @@ -15,7 +15,15 @@ describe ProjectSnippetPolicy, models: true do def abilities(user, snippet_visibility) snippet = create(:project_snippet, snippet_visibility, project: project) - described_class.abilities(user, snippet).to_set + described_class.new(user, snippet) + end + + def expect_allowed(*permissions) + permissions.each { |p| is_expected.to be_allowed(p) } + end + + def expect_disallowed(*permissions) + permissions.each { |p| is_expected.not_to be_allowed(p) } end context 'public snippet' do @@ -23,8 +31,8 @@ describe ProjectSnippetPolicy, models: true do subject { abilities(nil, :public) } it do - is_expected.to include(:read_project_snippet) - is_expected.not_to include(*author_permissions) + expect_allowed(:read_project_snippet) + expect_disallowed(*author_permissions) end end @@ -32,8 +40,8 @@ describe ProjectSnippetPolicy, models: true do subject { abilities(regular_user, :public) } it do - is_expected.to include(:read_project_snippet) - is_expected.not_to include(*author_permissions) + expect_allowed(:read_project_snippet) + expect_disallowed(*author_permissions) end end @@ -41,8 +49,8 @@ describe ProjectSnippetPolicy, models: true do subject { abilities(external_user, :public) } it do - is_expected.to include(:read_project_snippet) - is_expected.not_to include(*author_permissions) + expect_allowed(:read_project_snippet) + expect_disallowed(*author_permissions) end end end @@ -52,8 +60,8 @@ describe ProjectSnippetPolicy, models: true do subject { abilities(nil, :internal) } it do - is_expected.not_to include(:read_project_snippet) - is_expected.not_to include(*author_permissions) + expect_disallowed(:read_project_snippet) + expect_disallowed(*author_permissions) end end @@ -61,8 +69,8 @@ describe ProjectSnippetPolicy, models: true do subject { abilities(regular_user, :internal) } it do - is_expected.to include(:read_project_snippet) - is_expected.not_to include(*author_permissions) + expect_allowed(:read_project_snippet) + expect_disallowed(*author_permissions) end end @@ -70,8 +78,8 @@ describe ProjectSnippetPolicy, models: true do subject { abilities(external_user, :internal) } it do - is_expected.not_to include(:read_project_snippet) - is_expected.not_to include(*author_permissions) + expect_disallowed(:read_project_snippet) + expect_disallowed(*author_permissions) end end @@ -83,8 +91,8 @@ describe ProjectSnippetPolicy, models: true do end it do - is_expected.to include(:read_project_snippet) - is_expected.not_to include(*author_permissions) + expect_allowed(:read_project_snippet) + expect_disallowed(*author_permissions) end end end @@ -94,8 +102,8 @@ describe ProjectSnippetPolicy, models: true do subject { abilities(nil, :private) } it do - is_expected.not_to include(:read_project_snippet) - is_expected.not_to include(*author_permissions) + expect_disallowed(:read_project_snippet) + expect_disallowed(*author_permissions) end end @@ -103,19 +111,19 @@ describe ProjectSnippetPolicy, models: true do subject { abilities(regular_user, :private) } it do - is_expected.not_to include(:read_project_snippet) - is_expected.not_to include(*author_permissions) + expect_disallowed(:read_project_snippet) + expect_disallowed(*author_permissions) end end context 'snippet author' do let(:snippet) { create(:project_snippet, :private, author: regular_user, project: project) } - subject { described_class.abilities(regular_user, snippet).to_set } + subject { described_class.new(regular_user, snippet) } it do - is_expected.to include(:read_project_snippet) - is_expected.to include(*author_permissions) + expect_allowed(:read_project_snippet) + expect_allowed(*author_permissions) end end @@ -127,8 +135,8 @@ describe ProjectSnippetPolicy, models: true do end it do - is_expected.to include(:read_project_snippet) - is_expected.not_to include(*author_permissions) + expect_allowed(:read_project_snippet) + expect_disallowed(*author_permissions) end end @@ -140,8 +148,8 @@ describe ProjectSnippetPolicy, models: true do end it do - is_expected.to include(:read_project_snippet) - is_expected.not_to include(*author_permissions) + expect_allowed(:read_project_snippet) + expect_disallowed(*author_permissions) end end @@ -149,8 +157,8 @@ describe ProjectSnippetPolicy, models: true do subject { abilities(create(:admin), :private) } it do - is_expected.to include(:read_project_snippet) - is_expected.to include(*author_permissions) + expect_allowed(:read_project_snippet) + expect_allowed(*author_permissions) end end end diff --git a/spec/policies/user_policy_spec.rb b/spec/policies/user_policy_spec.rb index d5761390d39..0251d5dcf1c 100644 --- a/spec/policies/user_policy_spec.rb +++ b/spec/policies/user_policy_spec.rb @@ -4,34 +4,34 @@ describe UserPolicy, models: true do let(:current_user) { create(:user) } let(:user) { create(:user) } - subject { described_class.abilities(current_user, user).to_set } + subject { UserPolicy.new(current_user, user) } describe "reading a user's information" do - it { is_expected.to include(:read_user) } + it { is_expected.to be_allowed(:read_user) } end describe "destroying a user" do context "when a regular user tries to destroy another regular user" do - it { is_expected.not_to include(:destroy_user) } + it { is_expected.not_to be_allowed(:destroy_user) } end context "when a regular user tries to destroy themselves" do let(:current_user) { user } - it { is_expected.to include(:destroy_user) } + it { is_expected.to be_allowed(:destroy_user) } end context "when an admin user tries to destroy a regular user" do let(:current_user) { create(:user, :admin) } - it { is_expected.to include(:destroy_user) } + it { is_expected.to be_allowed(:destroy_user) } end context "when an admin user tries to destroy a ghost user" do let(:current_user) { create(:user, :admin) } let(:user) { create(:user, :ghost) } - it { is_expected.not_to include(:destroy_user) } + it { is_expected.not_to be_allowed(:destroy_user) } end end end diff --git a/spec/requests/api/namespaces_spec.rb b/spec/requests/api/namespaces_spec.rb index 3bf16a3ae27..26cf653ca8e 100644 --- a/spec/requests/api/namespaces_spec.rb +++ b/spec/requests/api/namespaces_spec.rb @@ -15,6 +15,20 @@ describe API::Namespaces do end context "when authenticated as admin" do + it "returns correct attributes" do + get api("/namespaces", admin) + + group_kind_json_response = json_response.find { |resource| resource['kind'] == 'group' } + user_kind_json_response = json_response.find { |resource| resource['kind'] == 'user' } + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(group_kind_json_response.keys).to contain_exactly('id', 'kind', 'name', 'path', 'full_path', + 'parent_id', 'members_count_with_descendants') + + expect(user_kind_json_response.keys).to contain_exactly('id', 'kind', 'name', 'path', 'full_path', 'parent_id') + end + it "admin: returns an array of all namespaces" do get api("/namespaces", admin) @@ -37,6 +51,27 @@ describe API::Namespaces do end context "when authenticated as a regular user" do + it "returns correct attributes when user can admin group" do + group1.add_owner(user) + + get api("/namespaces", user) + + owned_group_response = json_response.find { |resource| resource['id'] == group1.id } + + expect(owned_group_response.keys).to contain_exactly('id', 'kind', 'name', 'path', 'full_path', + 'parent_id', 'members_count_with_descendants') + end + + it "returns correct attributes when user cannot admin group" do + group1.add_guest(user) + + get api("/namespaces", user) + + guest_group_response = json_response.find { |resource| resource['id'] == group1.id } + + expect(guest_group_response.keys).to contain_exactly('id', 'kind', 'name', 'path', 'full_path', 'parent_id') + end + it "user: returns an array of namespaces" do get api("/namespaces", user) diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index fd7ff0b9cff..14dec3d45b1 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -698,7 +698,8 @@ describe API::Projects do 'name' => user.namespace.name, 'path' => user.namespace.path, 'kind' => user.namespace.kind, - 'full_path' => user.namespace.full_path + 'full_path' => user.namespace.full_path, + 'parent_id' => nil }) end diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb index cb74868324c..af44ffa2331 100644 --- a/spec/requests/api/v3/projects_spec.rb +++ b/spec/requests/api/v3/projects_spec.rb @@ -734,7 +734,8 @@ describe API::V3::Projects do 'name' => user.namespace.name, 'path' => user.namespace.path, 'kind' => user.namespace.kind, - 'full_path' => user.namespace.full_path + 'full_path' => user.namespace.full_path, + 'parent_id' => nil }) end diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index 95d40138fea..2f1c3c95e59 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -246,28 +246,13 @@ describe 'project routing' do end end - # diffs_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id/diffs(.:format) projects/merge_requests#diffs - # commits_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id/commits(.:format) projects/merge_requests#commits - # merge_namespace_project_merge_request POST /:namespace_id/:project_id/merge_requests/:id/merge(.:format) projects/merge_requests#merge - # ci_status_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id/ci_status(.:format) projects/merge_requests#ci_status - # toggle_subscription_namespace_project_merge_request POST /:namespace_id/:project_id/merge_requests/:id/toggle_subscription(.:format) projects/merge_requests#toggle_subscription - # branch_from_namespace_project_merge_requests GET /:namespace_id/:project_id/merge_requests/branch_from(.:format) projects/merge_requests#branch_from - # branch_to_namespace_project_merge_requests GET /:namespace_id/:project_id/merge_requests/branch_to(.:format) projects/merge_requests#branch_to - # update_branches_namespace_project_merge_requests GET /:namespace_id/:project_id/merge_requests/update_branches(.:format) projects/merge_requests#update_branches - # namespace_project_merge_requests GET /:namespace_id/:project_id/merge_requests(.:format) projects/merge_requests#index - # POST /:namespace_id/:project_id/merge_requests(.:format) projects/merge_requests#create - # new_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/new(.:format) projects/merge_requests#new - # edit_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id/edit(.:format) projects/merge_requests#edit - # namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id(.:format) projects/merge_requests#show - # PATCH /:namespace_id/:project_id/merge_requests/:id(.:format) projects/merge_requests#update - # PUT /:namespace_id/:project_id/merge_requests/:id(.:format) projects/merge_requests#update describe Projects::MergeRequestsController, 'routing' do - it 'to #diffs' do - expect(get('/gitlab/gitlabhq/merge_requests/1/diffs')).to route_to('projects/merge_requests#diffs', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') + it 'to #commits' do + expect(get('/gitlab/gitlabhq/merge_requests/1/commits.json')).to route_to('projects/merge_requests#commits', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', format: 'json') end - it 'to #commits' do - expect(get('/gitlab/gitlabhq/merge_requests/1/commits')).to route_to('projects/merge_requests#commits', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') + it 'to #pipelines' do + expect(get('/gitlab/gitlabhq/merge_requests/1/pipelines.json')).to route_to('projects/merge_requests#pipelines', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', format: 'json') end it 'to #merge' do @@ -277,25 +262,59 @@ describe 'project routing' do ) end + it 'to #show' do + expect(get('/gitlab/gitlabhq/merge_requests/1.diff')).to route_to('projects/merge_requests#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', format: 'diff') + expect(get('/gitlab/gitlabhq/merge_requests/1.patch')).to route_to('projects/merge_requests#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', format: 'patch') + expect(get('/gitlab/gitlabhq/merge_requests/1/diffs')).to route_to('projects/merge_requests#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', tab: 'diffs') + expect(get('/gitlab/gitlabhq/merge_requests/1/commits')).to route_to('projects/merge_requests#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', tab: 'commits') + expect(get('/gitlab/gitlabhq/merge_requests/1/pipelines')).to route_to('projects/merge_requests#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', tab: 'pipelines') + end + + it_behaves_like 'RESTful project resources' do + let(:controller) { 'merge_requests' } + let(:actions) { [:index, :edit, :show, :update] } + end + end + + describe Projects::MergeRequests::CreationsController, 'routing' do + it 'to #new' do + expect(get('/gitlab/gitlabhq/merge_requests/new')).to route_to('projects/merge_requests/creations#new', namespace_id: 'gitlab', project_id: 'gitlabhq') + expect(get('/gitlab/gitlabhq/merge_requests/new/diffs')).to route_to('projects/merge_requests/creations#new', namespace_id: 'gitlab', project_id: 'gitlabhq', tab: 'diffs') + expect(get('/gitlab/gitlabhq/merge_requests/new/pipelines')).to route_to('projects/merge_requests/creations#new', namespace_id: 'gitlab', project_id: 'gitlabhq', tab: 'pipelines') + end + + it 'to #create' do + expect(post('/gitlab/gitlabhq/merge_requests')).to route_to('projects/merge_requests/creations#create', namespace_id: 'gitlab', project_id: 'gitlabhq') + end + it 'to #branch_from' do - expect(get('/gitlab/gitlabhq/merge_requests/branch_from')).to route_to('projects/merge_requests#branch_from', namespace_id: 'gitlab', project_id: 'gitlabhq') + expect(get('/gitlab/gitlabhq/merge_requests/new/branch_from')).to route_to('projects/merge_requests/creations#branch_from', namespace_id: 'gitlab', project_id: 'gitlabhq') end it 'to #branch_to' do - expect(get('/gitlab/gitlabhq/merge_requests/branch_to')).to route_to('projects/merge_requests#branch_to', namespace_id: 'gitlab', project_id: 'gitlabhq') + expect(get('/gitlab/gitlabhq/merge_requests/new/branch_to')).to route_to('projects/merge_requests/creations#branch_to', namespace_id: 'gitlab', project_id: 'gitlabhq') end - it 'to #show' do - expect(get('/gitlab/gitlabhq/merge_requests/1.diff')).to route_to('projects/merge_requests#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', format: 'diff') - expect(get('/gitlab/gitlabhq/merge_requests/1.patch')).to route_to('projects/merge_requests#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', format: 'patch') + it 'to #pipelines' do + expect(get('/gitlab/gitlabhq/merge_requests/new/pipelines.json')).to route_to('projects/merge_requests/creations#pipelines', namespace_id: 'gitlab', project_id: 'gitlabhq', format: 'json') end - it_behaves_like 'RESTful project resources' do - let(:controller) { 'merge_requests' } - let(:actions) { [:index, :create, :new, :edit, :show, :update] } + it 'to #diffs' do + expect(get('/gitlab/gitlabhq/merge_requests/new/diffs.json')).to route_to('projects/merge_requests/creations#diffs', namespace_id: 'gitlab', project_id: 'gitlabhq', format: 'json') + end + end + + describe Projects::MergeRequests::DiffsController, 'routing' do + it 'to #show' do + expect(get('/gitlab/gitlabhq/merge_requests/1/diffs.json')).to route_to('projects/merge_requests/diffs#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', format: 'json') end end + describe Projects::MergeRequests::ConflictsController, 'routing' do + it 'to #show' do + expect(get('/gitlab/gitlabhq/merge_requests/1/conflicts')).to route_to('projects/merge_requests/conflicts#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') + end + end # raw_project_snippet GET /:project_id/snippets/:id/raw(.:format) snippets#raw # project_snippets GET /:project_id/snippets(.:format) snippets#index # POST /:project_id/snippets(.:format) snippets#create diff --git a/spec/support/fake_migration_classes.rb b/spec/support/fake_migration_classes.rb index 3de0460c3ca..b0fc8422857 100644 --- a/spec/support/fake_migration_classes.rb +++ b/spec/support/fake_migration_classes.rb @@ -1,3 +1,11 @@ class FakeRenameReservedPathMigrationV1 < ActiveRecord::Migration include Gitlab::Database::RenameReservedPathsMigration::V1 + + def version + '20170316163845' + end + + def name + "FakeRenameReservedPathMigrationV1" + end end diff --git a/spec/support/features/issuable_slash_commands_shared_examples.rb b/spec/support/features/issuable_slash_commands_shared_examples.rb index 50869099bb7..98b014df6cd 100644 --- a/spec/support/features/issuable_slash_commands_shared_examples.rb +++ b/spec/support/features/issuable_slash_commands_shared_examples.rb @@ -28,7 +28,12 @@ shared_examples 'issuable record that supports quick actions in its description describe "new #{issuable_type}", js: true do context 'with commands in the description' do it "creates the #{issuable_type} and interpret commands accordingly" do - visit public_send("new_namespace_project_#{issuable_type}_path", project.namespace, project, new_url_opts) + case issuable_type + when :merge_request + visit public_send("namespace_project_new_merge_request_path", project.namespace, project, new_url_opts) + when :issue + visit public_send("new_namespace_project_issue_path", project.namespace, project, new_url_opts) + end fill_in "#{issuable_type}_title", with: 'bug 345' fill_in "#{issuable_type}_description", with: "bug description\n/label ~bug\n/milestone %\"ASAP\"" click_button "Submit #{issuable_type}".humanize diff --git a/spec/support/matchers/access_matchers_for_controller.rb b/spec/support/matchers/access_matchers_for_controller.rb new file mode 100644 index 00000000000..fb43f51c70c --- /dev/null +++ b/spec/support/matchers/access_matchers_for_controller.rb @@ -0,0 +1,84 @@ +# AccessMatchersForController +# +# For testing authorize_xxx in controller. +module AccessMatchersForController + extend RSpec::Matchers::DSL + include Warden::Test::Helpers + + EXPECTED_STATUS_CODE_ALLOWED = [200, 201, 302].freeze + EXPECTED_STATUS_CODE_DENIED = [401, 404].freeze + + def emulate_user(role, membership = nil) + case role + when :admin + user = create(:admin) + sign_in(user) + when :user + user = create(:user) + sign_in(user) + when :external + user = create(:user, external: true) + sign_in(user) + when :visitor + user = nil + when User + user = role + sign_in(user) + when *Gitlab::Access.sym_options_with_owner.keys # owner, master, developer, reporter, guest + raise ArgumentError, "cannot emulate #{role} without membership parent" unless membership + + user = create_user_by_membership(role, membership) + sign_in(user) + else + raise ArgumentError, "cannot emulate user #{role}" + end + + user + end + + def create_user_by_membership(role, membership) + if role == :owner && membership.owner + user = membership.owner + else + user = create(:user) + membership.public_send(:"add_#{role}", user) + end + user + end + + def description_for(role, type, expected, result) + "be #{type} for #{role}. Expected: #{expected.join(',')} Got: #{result}" + end + + matcher :be_allowed_for do |role| + match do |action| + emulate_user(role, @membership) + action.call + + EXPECTED_STATUS_CODE_ALLOWED.include?(response.status) + end + + chain :of do |membership| + @membership = membership + end + + description { description_for(role, 'allowed', EXPECTED_STATUS_CODE_ALLOWED, response.status) } + supports_block_expectations + end + + matcher :be_denied_for do |role| + match do |action| + emulate_user(role, @membership) + action.call + + EXPECTED_STATUS_CODE_DENIED.include?(response.status) + end + + chain :of do |membership| + @membership = membership + end + + description { description_for(role, 'denied', EXPECTED_STATUS_CODE_DENIED, response.status) } + supports_block_expectations + end +end diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 1c5267c290b..32546abcad4 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -120,18 +120,21 @@ module TestEnv end def setup_gitlab_shell - unless File.directory?(Gitlab.config.gitlab_shell.path) - unless system('rake', 'gitlab:shell:install') - raise 'Can`t clone gitlab-shell' - end + shell_needs_update = component_needs_update?(Gitlab.config.gitlab_shell.path, + Gitlab::Shell.version_required) + + unless !shell_needs_update || system('rake', 'gitlab:shell:install') + raise 'Can`t clone gitlab-shell' end end def setup_gitaly socket_path = Gitlab::GitalyClient.address('default').sub(/\Aunix:/, '') gitaly_dir = File.dirname(socket_path) + gitaly_needs_update = component_needs_update?(gitaly_dir, + Gitlab::GitalyClient.expected_server_version) - unless !gitaly_needs_update?(gitaly_dir) || system('rake', "gitlab:gitaly:install[#{gitaly_dir}]") + unless !gitaly_needs_update || system('rake', "gitlab:gitaly:install[#{gitaly_dir}]") raise "Can't clone gitaly" end @@ -261,13 +264,13 @@ module TestEnv end end - def gitaly_needs_update?(gitaly_dir) - gitaly_version = File.read(File.join(gitaly_dir, 'VERSION')).strip + def component_needs_update?(component_folder, expected_version) + version = File.read(File.join(component_folder, 'VERSION')).strip # Notice that this will always yield true when using branch versions # (`=branch_name`), but that actually makes sure the server is always based # on the latest branch revision. - gitaly_version != Gitlab::GitalyClient.expected_server_version + version != expected_version rescue Errno::ENOENT true end diff --git a/spec/views/projects/merge_requests/_commits.html.haml_spec.rb b/spec/views/projects/merge_requests/_commits.html.haml_spec.rb index 4052dbf8df3..3e17fe2104b 100644 --- a/spec/views/projects/merge_requests/_commits.html.haml_spec.rb +++ b/spec/views/projects/merge_requests/_commits.html.haml_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'projects/merge_requests/show/_commits.html.haml' do +describe 'projects/merge_requests/_commits.html.haml' do include Devise::Test::ControllerHelpers let(:user) { create(:user) } diff --git a/spec/views/projects/merge_requests/_new_submit.html.haml_spec.rb b/spec/views/projects/merge_requests/creations/_new_submit.html.haml_spec.rb index 4f698a34ab5..1e9bdf9108f 100644 --- a/spec/views/projects/merge_requests/_new_submit.html.haml_spec.rb +++ b/spec/views/projects/merge_requests/creations/_new_submit.html.haml_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'projects/merge_requests/_new_submit.html.haml', :view do +describe 'projects/merge_requests/creations/_new_submit.html.haml', :view do let(:merge_request) { create(:merge_request) } let!(:pipeline) { create(:ci_empty_pipeline) } diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb index cc9bc29c6cc..a8f4bb72acf 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -4,7 +4,7 @@ describe PostReceive do let(:changes) { "123456 789012 refs/heads/tést\n654321 210987 refs/tags/tag" } let(:wrongly_encoded_changes) { changes.encode("ISO-8859-1").force_encoding("UTF-8") } let(:base64_changes) { Base64.encode64(wrongly_encoded_changes) } - let(:project_identifier) { "project-#{project.id}" } + let(:gl_repository) { "project-#{project.id}" } let(:key) { create(:key, user: project.owner) } let(:key_id) { key.shell_id } @@ -19,22 +19,14 @@ describe PostReceive do end context 'with a non-existing project' do - let(:project_identifier) { "project-123456789" } + let(:gl_repository) { "project-123456789" } let(:error_message) do - "Triggered hook for non-existing project with identifier \"#{project_identifier}\"" + "Triggered hook for non-existing project with gl_repository \"#{gl_repository}\"" end it "returns false and logs an error" do expect(Gitlab::GitLogger).to receive(:error).with("POST-RECEIVE: #{error_message}") - expect(described_class.new.perform(project_identifier, key_id, base64_changes)).to be(false) - end - end - - context "with an absolute path as the project identifier" do - it "searches the project by full path" do - expect(Project).to receive(:find_by_full_path).with(project.full_path, follow_redirects: true).and_call_original - - described_class.new.perform(pwd(project), key_id, base64_changes) + expect(described_class.new.perform(gl_repository, key_id, base64_changes)).to be(false) end end @@ -49,7 +41,7 @@ describe PostReceive do it "calls GitTagPushService" do expect_any_instance_of(GitPushService).to receive(:execute).and_return(true) expect_any_instance_of(GitTagPushService).not_to receive(:execute) - described_class.new.perform(project_identifier, key_id, base64_changes) + described_class.new.perform(gl_repository, key_id, base64_changes) end end @@ -59,7 +51,7 @@ describe PostReceive do it "calls GitTagPushService" do expect_any_instance_of(GitPushService).not_to receive(:execute) expect_any_instance_of(GitTagPushService).to receive(:execute).and_return(true) - described_class.new.perform(project_identifier, key_id, base64_changes) + described_class.new.perform(gl_repository, key_id, base64_changes) end end @@ -69,12 +61,12 @@ describe PostReceive do it "does not call any of the services" do expect_any_instance_of(GitPushService).not_to receive(:execute) expect_any_instance_of(GitTagPushService).not_to receive(:execute) - described_class.new.perform(project_identifier, key_id, base64_changes) + described_class.new.perform(gl_repository, key_id, base64_changes) end end context "gitlab-ci.yml" do - subject { described_class.new.perform(project_identifier, key_id, base64_changes) } + subject { described_class.new.perform(gl_repository, key_id, base64_changes) } context "creates a Ci::Pipeline for every change" do before do @@ -111,7 +103,7 @@ describe PostReceive do it 'calls SystemHooksService' do expect_any_instance_of(SystemHooksService).to receive(:execute_hooks).with(fake_hook_data, :repository_update_hooks).and_return(true) - described_class.new.perform(project_identifier, key_id, base64_changes) + described_class.new.perform(gl_repository, key_id, base64_changes) end end end @@ -119,7 +111,7 @@ describe PostReceive do context "webhook" do it "fetches the correct project" do expect(Project).to receive(:find_by).with(id: project.id.to_s) - described_class.new.perform(project_identifier, key_id, base64_changes) + described_class.new.perform(gl_repository, key_id, base64_changes) end it "does not run if the author is not in the project" do @@ -129,7 +121,7 @@ describe PostReceive do expect(project).not_to receive(:execute_hooks) - expect(described_class.new.perform(project_identifier, key_id, base64_changes)).to be_falsey + expect(described_class.new.perform(gl_repository, key_id, base64_changes)).to be_falsey end it "asks the project to trigger all hooks" do @@ -137,18 +129,14 @@ describe PostReceive do expect(project).to receive(:execute_hooks).twice expect(project).to receive(:execute_services).twice - described_class.new.perform(project_identifier, key_id, base64_changes) + described_class.new.perform(gl_repository, key_id, base64_changes) end it "enqueues a UpdateMergeRequestsWorker job" do allow(Project).to receive(:find_by).and_return(project) expect(UpdateMergeRequestsWorker).to receive(:perform_async).with(project.id, project.owner.id, any_args) - described_class.new.perform(project_identifier, key_id, base64_changes) + described_class.new.perform(gl_repository, key_id, base64_changes) end end - - def pwd(project) - File.join(Gitlab.config.repositories.storages.default['path'], project.path_with_namespace) - end end |