Delete unauthorized Todos when project is private
Delete Todos for guest users when project visibility level is updated to private.
This commit is contained in:
		
							parent
							
								
									c9da437599
								
							
						
					
					
						commit
						be33946819
					
				| 
						 | 
				
			
			@ -38,7 +38,9 @@ class Todo < ApplicationRecord
 | 
			
		|||
      self
 | 
			
		||||
    end
 | 
			
		||||
  }, polymorphic: true, touch: true # rubocop:disable Cop/PolymorphicAssociations
 | 
			
		||||
 | 
			
		||||
  belongs_to :user
 | 
			
		||||
  belongs_to :issue, -> { where("target_type = 'Issue'") }, foreign_key: :target_id
 | 
			
		||||
 | 
			
		||||
  delegate :name, :email, to: :author, prefix: true, allow_nil: true
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -59,6 +61,7 @@ class Todo < ApplicationRecord
 | 
			
		|||
  scope :for_target, -> (id) { where(target_id: id) }
 | 
			
		||||
  scope :for_commit, -> (id) { where(commit_id: id) }
 | 
			
		||||
  scope :with_api_entity_associations, -> { preload(:target, :author, :note, group: :route, project: [:route, { namespace: :route }]) }
 | 
			
		||||
  scope :joins_issue_and_assignees, -> { left_joins(issue: :assignees) }
 | 
			
		||||
 | 
			
		||||
  state_machine :state, initial: :pending do
 | 
			
		||||
    event :done do
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -64,6 +64,7 @@ module Projects
 | 
			
		|||
 | 
			
		||||
      if project.previous_changes.include?(:visibility_level) && project.private?
 | 
			
		||||
        # don't enqueue immediately to prevent todos removal in case of a mistake
 | 
			
		||||
        TodosDestroyer::ConfidentialIssueWorker.perform_in(Todo::WAIT_FOR_DELETE, nil, project.id)
 | 
			
		||||
        TodosDestroyer::ProjectPrivateWorker.perform_in(Todo::WAIT_FOR_DELETE, project.id)
 | 
			
		||||
      elsif (project_changed_feature_keys & todos_features_changes).present?
 | 
			
		||||
        TodosDestroyer::PrivateFeaturesWorker.perform_in(Todo::WAIT_FOR_DELETE, project.id)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,7 +13,7 @@ module Todos
 | 
			
		|||
 | 
			
		||||
      # rubocop: disable CodeReuse/ActiveRecord
 | 
			
		||||
      def without_authorized(items)
 | 
			
		||||
        items.where('user_id NOT IN (?)', authorized_users)
 | 
			
		||||
        items.where('todos.user_id NOT IN (?)', authorized_users)
 | 
			
		||||
      end
 | 
			
		||||
      # rubocop: enable CodeReuse/ActiveRecord
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,36 +2,55 @@
 | 
			
		|||
 | 
			
		||||
module Todos
 | 
			
		||||
  module Destroy
 | 
			
		||||
    # Service class for deleting todos that belongs to confidential issues.
 | 
			
		||||
    # It deletes todos for users that are not at least reporters, issue author or assignee.
 | 
			
		||||
    #
 | 
			
		||||
    # Accepts issue_id or project_id as argument.
 | 
			
		||||
    # When issue_id is passed it deletes matching todos for one confidential issue.
 | 
			
		||||
    # When project_id is passed it deletes matching todos for all confidential issues of the project.
 | 
			
		||||
    class ConfidentialIssueService < ::Todos::Destroy::BaseService
 | 
			
		||||
      extend ::Gitlab::Utils::Override
 | 
			
		||||
 | 
			
		||||
      attr_reader :issue
 | 
			
		||||
      attr_reader :issues
 | 
			
		||||
 | 
			
		||||
      # rubocop: disable CodeReuse/ActiveRecord
 | 
			
		||||
      def initialize(issue_id)
 | 
			
		||||
        @issue = Issue.find_by(id: issue_id)
 | 
			
		||||
      def initialize(issue_id: nil, project_id: nil)
 | 
			
		||||
        @issues =
 | 
			
		||||
          if issue_id
 | 
			
		||||
            Issue.where(id: issue_id)
 | 
			
		||||
          elsif project_id
 | 
			
		||||
            project_confidential_issues(project_id)
 | 
			
		||||
          end
 | 
			
		||||
      end
 | 
			
		||||
      # rubocop: enable CodeReuse/ActiveRecord
 | 
			
		||||
 | 
			
		||||
      private
 | 
			
		||||
 | 
			
		||||
      def project_confidential_issues(project_id)
 | 
			
		||||
        project = Project.find(project_id)
 | 
			
		||||
 | 
			
		||||
        project.issues.confidential_only
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      override :todos
 | 
			
		||||
      # rubocop: disable CodeReuse/ActiveRecord
 | 
			
		||||
      def todos
 | 
			
		||||
        Todo.where(target: issue)
 | 
			
		||||
          .where('user_id != ?', issue.author_id)
 | 
			
		||||
          .where('user_id NOT IN (?)', issue.assignees.select(:id))
 | 
			
		||||
        Todo.joins_issue_and_assignees
 | 
			
		||||
          .where(target: issues)
 | 
			
		||||
          .where('issues.confidential = ?', true)
 | 
			
		||||
          .where('todos.user_id != issues.author_id')
 | 
			
		||||
          .where('todos.user_id != issue_assignees.user_id')
 | 
			
		||||
      end
 | 
			
		||||
      # rubocop: enable CodeReuse/ActiveRecord
 | 
			
		||||
 | 
			
		||||
      override :todos_to_remove?
 | 
			
		||||
      def todos_to_remove?
 | 
			
		||||
        issue&.confidential?
 | 
			
		||||
        issues&.any?(&:confidential?)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      override :project_ids
 | 
			
		||||
      def project_ids
 | 
			
		||||
        issue.project_id
 | 
			
		||||
        issues&.distinct&.select(:project_id)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      override :authorized_users
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,8 +5,8 @@ module TodosDestroyer
 | 
			
		|||
    include ApplicationWorker
 | 
			
		||||
    include TodosDestroyerQueue
 | 
			
		||||
 | 
			
		||||
    def perform(issue_id)
 | 
			
		||||
      ::Todos::Destroy::ConfidentialIssueService.new(issue_id).execute
 | 
			
		||||
    def perform(issue_id = nil, project_id = nil)
 | 
			
		||||
      ::Todos::Destroy::ConfidentialIssueService.new(issue_id: issue_id, project_id: project_id).execute
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,5 @@
 | 
			
		|||
---
 | 
			
		||||
title: Delete unauthorized Todos when project is made private
 | 
			
		||||
merge_request: 28560
 | 
			
		||||
author:
 | 
			
		||||
type: fixed
 | 
			
		||||
| 
						 | 
				
			
			@ -45,6 +45,7 @@ describe Projects::UpdateService do
 | 
			
		|||
 | 
			
		||||
        it 'updates the project to private' do
 | 
			
		||||
          expect(TodosDestroyer::ProjectPrivateWorker).to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, project.id)
 | 
			
		||||
          expect(TodosDestroyer::ConfidentialIssueWorker).to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, nil, project.id)
 | 
			
		||||
 | 
			
		||||
          result = update_project(project, user, visibility_level: Gitlab::VisibilityLevel::PRIVATE)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,28 +9,27 @@ describe Todos::Destroy::ConfidentialIssueService do
 | 
			
		|||
  let(:assignee)       { create(:user) }
 | 
			
		||||
  let(:guest)          { create(:user) }
 | 
			
		||||
  let(:project_member) { create(:user) }
 | 
			
		||||
  let(:issue)          { create(:issue, project: project, author: author, assignees: [assignee]) }
 | 
			
		||||
 | 
			
		||||
  let!(:todo_issue_non_member)   { create(:todo, user: user, target: issue, project: project) }
 | 
			
		||||
  let!(:todo_issue_member)       { create(:todo, user: project_member, target: issue, project: project) }
 | 
			
		||||
  let!(:todo_issue_author)       { create(:todo, user: author, target: issue, project: project) }
 | 
			
		||||
  let!(:todo_issue_asignee)      { create(:todo, user: assignee, target: issue, project: project) }
 | 
			
		||||
  let!(:todo_issue_guest)        { create(:todo, user: guest, target: issue, project: project) }
 | 
			
		||||
  let!(:todo_another_non_member) { create(:todo, user: user, project: project) }
 | 
			
		||||
  let(:issue_1)        { create(:issue, :confidential, project: project, author: author, assignees: [assignee]) }
 | 
			
		||||
 | 
			
		||||
  describe '#execute' do
 | 
			
		||||
    before do
 | 
			
		||||
      project.add_developer(project_member)
 | 
			
		||||
      project.add_guest(guest)
 | 
			
		||||
 | 
			
		||||
      # todos not to be deleted
 | 
			
		||||
      create(:todo, user: project_member, target: issue_1, project: project)
 | 
			
		||||
      create(:todo, user: author, target: issue_1, project: project)
 | 
			
		||||
      create(:todo, user: assignee, target: issue_1, project: project)
 | 
			
		||||
      create(:todo, user: user, project: project)
 | 
			
		||||
      # Todos to be deleted
 | 
			
		||||
      create(:todo, user: guest, target: issue_1, project: project)
 | 
			
		||||
      create(:todo, user: user, target: issue_1, project: project)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    subject { described_class.new(issue.id).execute }
 | 
			
		||||
    subject { described_class.new(issue_id: issue_1.id).execute }
 | 
			
		||||
 | 
			
		||||
    context 'when issue_id parameter is present' do
 | 
			
		||||
      context 'when provided issue is confidential' do
 | 
			
		||||
      before do
 | 
			
		||||
        issue.update!(confidential: true)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
        it 'removes issue todos for users who can not access the confidential issue' do
 | 
			
		||||
          expect { subject }.to change { Todo.count }.from(6).to(4)
 | 
			
		||||
        end
 | 
			
		||||
| 
						 | 
				
			
			@ -38,8 +37,33 @@ describe Todos::Destroy::ConfidentialIssueService do
 | 
			
		|||
 | 
			
		||||
      context 'when provided issue is not confidential' do
 | 
			
		||||
        it 'does not remove any todos' do
 | 
			
		||||
          issue_1.update(confidential: false)
 | 
			
		||||
 | 
			
		||||
          expect { subject }.not_to change { Todo.count }
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when project_id parameter is present' do
 | 
			
		||||
      subject { described_class.new(issue_id: nil, project_id: project.id).execute }
 | 
			
		||||
 | 
			
		||||
      it 'removes issues todos for users that cannot access confidential issues' do
 | 
			
		||||
        issue_2 = create(:issue, :confidential, project: project)
 | 
			
		||||
        issue_3 = create(:issue, :confidential, project: project, author: author, assignees: [assignee])
 | 
			
		||||
        issue_4 = create(:issue, project: project)
 | 
			
		||||
        # Todos not to be deleted
 | 
			
		||||
        create(:todo, user: guest, target: issue_1, project: project)
 | 
			
		||||
        create(:todo, user: assignee, target: issue_1, project: project)
 | 
			
		||||
        create(:todo, user: project_member, target: issue_2, project: project)
 | 
			
		||||
        create(:todo, user: author, target: issue_3, project: project)
 | 
			
		||||
        create(:todo, user: user, target: issue_4, project: project)
 | 
			
		||||
        create(:todo, user: user, project: project)
 | 
			
		||||
        # Todos to be deleted
 | 
			
		||||
        create(:todo, user: user, target: issue_1, project: project)
 | 
			
		||||
        create(:todo, user: guest, target: issue_2, project: project)
 | 
			
		||||
 | 
			
		||||
        expect { subject }.to change { Todo.count }.from(14).to(10)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,12 +3,19 @@
 | 
			
		|||
require 'spec_helper'
 | 
			
		||||
 | 
			
		||||
describe TodosDestroyer::ConfidentialIssueWorker do
 | 
			
		||||
  it "calls the Todos::Destroy::ConfidentialIssueService with the params it was given" do
 | 
			
		||||
    service = double
 | 
			
		||||
  let(:service) { double }
 | 
			
		||||
 | 
			
		||||
    expect(::Todos::Destroy::ConfidentialIssueService).to receive(:new).with(100).and_return(service)
 | 
			
		||||
  it "calls the Todos::Destroy::ConfidentialIssueService with issue_id parameter" do
 | 
			
		||||
    expect(::Todos::Destroy::ConfidentialIssueService).to receive(:new).with(issue_id: 100, project_id: nil).and_return(service)
 | 
			
		||||
    expect(service).to receive(:execute)
 | 
			
		||||
 | 
			
		||||
    described_class.new.perform(100)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  it "calls the Todos::Destroy::ConfidentialIssueService with project_id parameter" do
 | 
			
		||||
    expect(::Todos::Destroy::ConfidentialIssueService).to receive(:new).with(issue_id: nil, project_id: 100).and_return(service)
 | 
			
		||||
    expect(service).to receive(:execute)
 | 
			
		||||
 | 
			
		||||
    described_class.new.perform(nil, 100)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue