Project

General

Profile

Feature #44424 » 0001-Add-Export-issues-and-Export-time-entries-permission.patch

Go MAEDA, 2026-09-07 03:33

View differences:

app/controllers/application_controller.rb
565 565
  private :valid_back_url?
566 566
  helper_method :valid_back_url?
567
  # Returns true if the current user has the given export permission on
568
  # the projects of all the records in scope (issues or time entries)
569
  def export_allowed?(permission, scope)
570
    user = User.current
571
    return true if user.admin?
572

  
573
    if @project
574
      # Deny if the user does not have the permission on @project itself
575
      return false unless user.allowed_to?(permission, @project)
576
      # @project has no subprojects, so no further check is needed
577
      return true if @project.leaf?
578
    else
579
      # Cross-project view: deny if none of the user's roles has the permission
580
      return false unless user.allowed_to?(permission, nil, :global => true)
581
    end
582

  
583
    # Deny if the scope contains records of a project where
584
    # the user does not have the permission
585
    !scope.where.not(:project_id => Project.allowed_to(user, permission).select(:id)).exists?
586
  end
587
  private :export_allowed?
588
  helper_method :export_allowed?
589

  
567 590
  # Redirects to the request referer if present, redirects to args or call block otherwise.
568 591
  def redirect_to_referer_or(*args, &block)
569 592
    if referer = request.headers["Referer"]
app/controllers/gantts_controller.rb
37 37
    @query.group_by = nil
38 38
    @gantt.query = @query if @query.valid?
39
    @export_allowed = export_allowed?(:export_issues, @query.valid? ? @query.base_scope : Issue.none)
40
    if (request.format.pdf? || request.format.png?) && !@export_allowed
41
      deny_access
42
      return
43
    end
44

  
39 45
    basename = (@project ? "#{@project.identifier}-" : '') + 'gantt'
40 46
    respond_to do |format|
app/controllers/issues_controller.rb
48 48
    retrieve_query(IssueQuery, use_session)
49 49
    if @query.valid?
50
      if (request.format.csv? || request.format.pdf?) && !export_allowed?(:export_issues, @query.base_scope)
51
        deny_access
52
        return
53
      end
54

  
50 55
      respond_to do |format|
51 56
        format.html do
52 57
          @issue_count = @query.issue_count
......
95 100
  end
96 101
  def show
102
    if request.format.pdf? && !User.current.allowed_to?(:export_issues, @project)
103
      deny_access
104
      return
105
    end
106

  
97 107
    if !api_request? || include_in_api_response?('journals')
98 108
      @journals = @issue.visible_journals_with_index
99 109
      @journals.reverse! if User.current.wants_comments_in_reverse_order?
app/controllers/reports_controller.rb
44 44
  def issue_report_details
45 45
    with_subprojects = Setting.display_subprojects_issues?
46
    @export_allowed = export_allowed?(:export_issues, Issue.visible(User.current, :project => @project, :with_subprojects => with_subprojects))
47
    if request.format.csv? && !@export_allowed
48
      deny_access
49
      return
50
    end
51

  
46 52
    case params[:detail]
47 53
    when "tracker"
48 54
      @field = "tracker_id"
app/controllers/timelog_controller.rb
43 43
  def index
44 44
    retrieve_time_entry_query
45
    if request.format.csv? && !export_allowed?(:export_time_entries, @query.base_scope)
46
      deny_access
47
      return
48
    end
49

  
45 50
    scope = time_entry_scope.
46 51
      preload(:issue => [:project, :tracker, :status, :assigned_to, :priority]).
47 52
      preload(:project, :user)
......
74 79
  def report
75 80
    retrieve_time_entry_query
81
    if request.format.csv? && !export_allowed?(:export_time_entries, @query.base_scope)
82
      deny_access
83
      return
84
    end
85

  
76 86
    scope = time_entry_scope
77 87
    @report = Redmine::Helpers::TimeReport.new(@project, params[:criteria], params[:columns], scope)
app/views/gantts/_chart.html.erb
249 249
  </ul>
250 250
</span>
251
<% if @export_allowed %>
251 252
<% other_formats_links do |f| %>
252 253
  <%= f.link_to_with_query_parameters 'PDF', gantt.params %>
253 254
  <%= f.link_to_with_query_parameters('PNG', gantt.params) if gantt.respond_to?('to_image') %>
254 255
<% end %>
256
<% end %>
app/views/issues/index.html.erb
34 34
<span class="pagination"><%= pagination_links_full @issue_pages, @issue_count %></span>
35 35
<% end %>
36
<% export_allowed = export_allowed?(:export_issues, @query.base_scope) %>
36 37
<% other_formats_links do |f| %>
38
  <% if export_allowed %>
37 39
  <%= f.link_to_with_query_parameters 'CSV', {}, :onclick => "showModal('csv-export-options', '350px'); return false;" %>
38 40
  <%= f.link_to_with_query_parameters 'PDF' %>
41
  <% end %>
39 42
  <%= f.link_to_with_query_parameters 'Atom', :key => User.current.atom_key %>
40 43
<% end %>
44
<% if export_allowed %>
41 45
<div id="csv-export-options" style="display:none;">
42 46
  <h3 class="title"><%= l(:label_export_options, :export_format => 'CSV') %></h3>
43 47
  <%= form_tag(_project_issues_path(@project, :format => 'csv'), :method => :get, :id => 'csv-export-form') do %>
......
72 76
  </p>
73 77
  <% end %>
74 78
</div>
79
<% end %>
75 80
<% end %>
76 81
<%= call_hook(:view_issues_index_bottom, { :issues => @issues, :project => @project, :query => @query }) %>
app/views/issues/show.html.erb
155 155
<%= render partial: 'action_menu_edit' unless User.current.wants_comments_in_reverse_order? %>
156 156
<% other_formats_links do |f| %>
157
  <%= f.link_to 'PDF' %>
157
  <%= f.link_to 'PDF' if User.current.allowed_to?(:export_issues, @project) %>
158 158
  <%= f.link_to 'Atom', :url => {:key => User.current.atom_key} %>
159 159
<% end %>
app/views/reports/_details.html.erb
36 36
</tr>
37 37
</tfoot>
38 38
</table>
39
<% if @export_allowed %>
39 40
<% other_formats_links do |f| %>
40 41
  <%= f.link_to_with_query_parameters 'CSV', {}, :onclick => "showModal('csv-export-options', '330px'); return false;" %>
41 42
<% end %>
......
50 51
  </p>
51 52
  <% end %>
52 53
</div>
54
<% end %>
53 55
<div class="issue-report-graph hide-when-print" data-controller="reports--details" data-reports--details-title-value="<%= l(:label_issues_by, @report_title) %>">
54 56
  <template data-reports--details-target="labels">
app/views/timelog/index.html.erb
28 28
<%= render :partial => 'list', :locals => { :entries => @entries }%>
29 29
<span class="pagination"><%= pagination_links_full @entry_pages, @entry_count %></span>
30
<% export_allowed = export_allowed?(:export_time_entries, @query.base_scope) %>
30 31
<% other_formats_links do |f| %>
31
  <%= f.link_to_with_query_parameters 'CSV', {}, :onclick => "showModal('csv-export-options', '330px'); return false;" %>
32
  <%= f.link_to_with_query_parameters('CSV', {}, :onclick => "showModal('csv-export-options', '330px'); return false;") if export_allowed %>
32 33
  <%= f.link_to_with_query_parameters 'Atom', :key => User.current.atom_key %>
33 34
<% end %>
35
<% if export_allowed %>
34 36
<div id="csv-export-options" style="display:none;">
35 37
  <h3 class="title"><%= l(:label_export_options, :export_format => 'CSV') %></h3>
36 38
  <%= form_tag(_time_entries_path(@project, nil, :format => 'csv'), :method => :get, :id => 'csv-export-form') do %>
......
62 64
</div>
63 65
<% end %>
64 66
<% end %>
67
<% end %>
65 68
<% content_for :sidebar do %>
66 69
  <%= render :partial => 'timelog/sidebar' %>
app/views/timelog/report.html.erb
66 66
</table>
67 67
</div>
68
<% if export_allowed?(:export_time_entries, @query.base_scope) %>
68 69
<% other_formats_links do |f| %>
69 70
  <%= f.link_to_with_query_parameters 'CSV', {}, :onclick => "showModal('csv-export-options', '330px'); return false;" %>
70 71
<% end %>
71
<% end %>
72 72
<div id="csv-export-options" style="display: none;">
73 73
  <h3 class="title"><%= l(:label_export_options, :export_format => 'CSV') %></h3>
74 74
  <%= export_csv_encoding_select_tag %>
......
81 81
</div>
82 82
<% end %>
83 83
<% end %>
84
<% end %>
85
<% end %>
84 86
<% content_for :sidebar do %>
85 87
  <%= render :partial => 'sidebar' %>
config/locales/en.yml
612 612
  permission_export_wiki_pages: Export wiki pages
613 613
  permission_manage_subtasks: Manage subtasks
614 614
  permission_manage_related_issues: Manage related issues
615
  permission_export_issues: Export issues
615 616
  permission_import_issues: Import issues
616 617
  permission_log_time_for_other_users: Log spent time for other users
617 618
  permission_view_project: View projects
......
1112 1113
  label_member_management_all_roles: All roles
1113 1114
  label_member_management_selected_roles_only: Only these roles
1114 1115
  label_import_issues: Import issues
1116
  permission_export_time_entries: Export time entries
1115 1117
  permission_import_time_entries: Import time entries
1116 1118
  label_select_file_to_import: Select the file to import
1117 1119
  label_fields_separator: Field separator
db/migrate/20260906000000_add_export_issues_and_export_time_entries_permissions.rb
1
class AddExportIssuesAndExportTimeEntriesPermissions < ActiveRecord::Migration[8.1]
2
  def up
3
    Role.find_each do |role|
4
      role.add_permission!(:export_issues) if role.has_permission?(:view_issues)
5
      role.add_permission!(:export_time_entries) if role.has_permission?(:view_time_entries)
6
    end
7
  end
8

  
9
  def down
10
    Role.find_each do |role|
11
      role.remove_permission!(:export_issues, :export_time_entries)
12
    end
13
  end
14
end
lib/redmine/default_data/loader.rb
59 59
                  :manage_versions,
60 60
                  :manage_categories,
61 61
                  :view_issues,
62
                  :export_issues,
62 63
                  :add_issues,
63 64
                  :edit_issues,
64 65
                  :view_private_notes,
......
71 72
                  :view_calendar,
72 73
                  :log_time,
73 74
                  :view_time_entries,
75
                  :export_time_entries,
74 76
                  :view_news,
75 77
                  :comment_news,
76 78
                  :view_documents,
......
95 97
                :position => 3,
96 98
                :permissions => [
97 99
                  :view_issues,
100
                  :export_issues,
98 101
                  :add_issues,
99 102
                  :add_issue_notes,
100 103
                  :save_queries,
......
102 105
                  :view_calendar,
103 106
                  :log_time,
104 107
                  :view_time_entries,
108
                  :export_time_entries,
105 109
                  :view_news,
106 110
                  :comment_news,
107 111
                  :view_documents,
......
116 120
                ]
117 121
              )
118 122
            Role.non_member.update_attribute :permissions, [:view_issues,
123
                                                            :export_issues,
119 124
                                                            :add_issues,
120 125
                                                            :add_issue_notes,
121 126
                                                            :save_queries,
122 127
                                                            :view_gantt,
123 128
                                                            :view_calendar,
124 129
                                                            :view_time_entries,
130
                                                            :export_time_entries,
125 131
                                                            :view_news,
126 132
                                                            :comment_news,
127 133
                                                            :view_documents,
lib/redmine/preparation.rb
82 82
          map.permission :view_issue_watchers, {}, :read => true
83 83
          map.permission :add_issue_watchers, {:watchers => [:new, :create, :append, :autocomplete_for_user, :autocomplete_for_mention]}
84 84
          map.permission :delete_issue_watchers, {:watchers => :destroy}
85
          map.permission :export_issues, {}, :read => true
85 86
          map.permission :import_issues, {}
86 87
          # Issue categories
87 88
          map.permission :manage_categories, {:projects => :settings, :issue_categories => [:index, :show, :new, :create, :edit, :update, :destroy]}, :require => :member
......
100 101
                         {:projects => :settings, :project_enumerations => [:update, :destroy]},
101 102
                         :require => :member
102 103
          map.permission :log_time_for_other_users, :require => :member
104
          map.permission :export_time_entries, {}, :read => true
103 105
          map.permission :import_time_entries, {}
104 106
        end
test/fixtures/roles.yml
17 17
    - :manage_versions
18 18
    - :manage_categories
19 19
    - :view_issues
20
    - :export_issues
20 21
    - :add_issues
21 22
    - :edit_issues
22 23
    - :copy_issues
......
36 37
    - :view_calendar
37 38
    - :log_time
38 39
    - :view_time_entries
40
    - :export_time_entries
39 41
    - :edit_time_entries
40 42
    - :delete_time_entries
41 43
    - :import_time_entries
......
91 93
    - :manage_versions
92 94
    - :manage_categories
93 95
    - :view_issues
96
    - :export_issues
94 97
    - :add_issues
95 98
    - :edit_issues
96 99
    - :copy_issues
......
104 107
    - :view_calendar
105 108
    - :log_time
106 109
    - :view_time_entries
110
    - :export_time_entries
107 111
    - :edit_own_time_entries
108 112
    - :view_news
109 113
    - :manage_news
......
144 148
    - :manage_versions
145 149
    - :manage_categories
146 150
    - :view_issues
151
    - :export_issues
147 152
    - :add_issues
148 153
    - :edit_issues
149 154
    - :manage_issue_relations
......
154 159
    - :view_calendar
155 160
    - :log_time
156 161
    - :view_time_entries
162
    - :export_time_entries
157 163
    - :view_news
158 164
    - :manage_news
159 165
    - :comment_news
......
185 191
  permissions: |
186 192
    ---
187 193
    - :view_issues
194
    - :export_issues
188 195
    - :add_issues
189 196
    - :edit_issues
190 197
    - :manage_issue_relations
......
194 201
    - :view_calendar
195 202
    - :log_time
196 203
    - :view_time_entries
204
    - :export_time_entries
197 205
    - :view_news
198 206
    - :comment_news
199 207
    - :view_documents
......
217 225
  permissions: |
218 226
    ---
219 227
    - :view_issues
228
    - :export_issues
220 229
    - :add_issue_notes
221 230
    - :view_gantt
222 231
    - :view_calendar
223 232
    - :view_time_entries
233
    - :export_time_entries
224 234
    - :view_news
225 235
    - :view_documents
226 236
    - :view_wiki_pages
test/functional/gantts_controller_test.rb
171 171
    assert @response.body.starts_with?('%PDF')
172 172
  end
173
  def test_gantt_pdf_and_png_without_export_issues_permission_should_be_denied
174
    @request.session[:user_id] = 2
175
    Role.find_by_name('Manager').remove_permission! :export_issues
176

  
177
    get :show, :params => {:project_id => 1, :format => 'pdf'}
178
    assert_response :forbidden
179

  
180
    get :show, :params => {:project_id => 1, :format => 'png'}
181
    assert_response :forbidden
182
  end
183

  
184
  def test_gantt_should_not_show_export_links_without_export_issues_permission
185
    @request.session[:user_id] = 2
186
    Role.find_by_name('Manager').remove_permission! :export_issues
187
    get :show, :params => {:project_id => 1}
188
    assert_response :success
189
    assert_select 'p.other-formats', 0
190
  end
191

  
173 192
  if Object.const_defined?(:MiniMagick) && convert_installed?
174 193
    def test_gantt_should_export_to_png
175 194
      get(
test/functional/issues_controller_test.rb
823 823
    assert_equal Setting.issue_list_default_columns.size + 2, lines[0].split(',').size
824 824
  end
825
  def test_index_csv_and_pdf_without_export_issues_permission_should_be_denied
826
    @request.session[:user_id] = 2
827
    Role.find_by_name('Manager').remove_permission! :export_issues
828

  
829
    get :index, :params => {:project_id => 1, :format => 'csv'}
830
    assert_response :forbidden
831

  
832
    get :index, :params => {:project_id => 1, :format => 'pdf'}
833
    assert_response :forbidden
834
  end
835

  
836
  def test_index_csv_across_projects_without_export_issues_permission_should_be_denied
837
    @request.session[:user_id] = 2
838
    Role.all.each {|role| role.remove_permission! :export_issues}
839
    get :index, :params => {:format => 'csv'}
840
    assert_response :forbidden
841
  end
842

  
843
  def test_index_csv_should_require_export_issues_permission_on_every_project_of_the_issues
844
    # User 2 is Developer on project 2 and has visible issues there
845
    @request.session[:user_id] = 2
846
    Role.find_by_name('Developer').remove_permission! :export_issues
847

  
848
    get :index, :params => {:format => 'csv'}
849
    assert_response :forbidden
850

  
851
    get :index, :params => {:project_id => 1, :format => 'csv'}
852
    assert_response :success
853
  end
854

  
855
  def test_index_should_not_show_export_links_without_export_issues_permission_on_every_project_of_the_issues
856
    @request.session[:user_id] = 2
857
    Role.find_by_name('Developer').remove_permission! :export_issues
858

  
859
    get :index
860
    assert_response :success
861
    assert_select 'p.other-formats a.csv', 0
862
    assert_select 'p.other-formats a.pdf', 0
863

  
864
    get :index, :params => {:project_id => 1}
865
    assert_response :success
866
    assert_select 'p.other-formats a.csv'
867
    assert_select 'p.other-formats a.pdf'
868
  end
869

  
870
  def test_index_should_not_show_export_links_without_export_issues_permission
871
    @request.session[:user_id] = 2
872
    Role.find_by_name('Manager').remove_permission! :export_issues
873
    get :index, :params => {:project_id => 1}
874
    assert_response :success
875

  
876
    assert_select 'p.other-formats a.csv', 0
877
    assert_select 'p.other-formats a.pdf', 0
878
    assert_select 'p.other-formats a.atom'
879
    assert_select '#csv-export-options', 0
880
  end
881

  
825 882
  def test_index_csv_filename_without_query_name_param
826 883
    get :index, :params => {:format => 'csv'}
827 884
    assert_response :success
......
3122 3179
    assert @response.body.starts_with?('%PDF')
3123 3180
  end
3181
  def test_show_pdf_without_export_issues_permission_should_be_denied
3182
    @request.session[:user_id] = 2
3183
    Role.find_by_name('Manager').remove_permission! :export_issues
3184
    get :show, :params => {:id => 1, :format => 'pdf'}
3185
    assert_response :forbidden
3186
  end
3187

  
3188
  def test_show_should_not_show_pdf_link_without_export_issues_permission
3189
    @request.session[:user_id] = 2
3190
    Role.find_by_name('Manager').remove_permission! :export_issues
3191
    get :show, :params => {:id => 1}
3192
    assert_response :success
3193

  
3194
    assert_select 'p.other-formats a.pdf', 0
3195
    assert_select 'p.other-formats a.atom'
3196
  end
3197

  
3124 3198
  def test_export_to_pdf_with_utf8_u_fffd
3125 3199
    issue = Issue.generate!(:subject => "�")
3126 3200
    ["en", "zh", "zh-TW", "ja", "ko", "ar"].each do |lang|
test/functional/reports_controller_test.rb
261 261
    end
262 262
  end
263
  def test_issue_report_details_csv_without_export_issues_permission_should_be_denied
264
    @request.session[:user_id] = 2
265
    Role.find_by_name('Manager').remove_permission! :export_issues
266
    get :issue_report_details, :params => {:id => 1, :detail => 'tracker', :format => 'csv'}
267
    assert_response :forbidden
268
  end
269

  
270
  def test_issue_report_details_csv_should_require_export_issues_permission_on_subprojects
271
    # User 2 is a non member of the public subproject 3 which has visible issues
272
    @request.session[:user_id] = 2
273
    Role.non_member.remove_permission! :export_issues
274

  
275
    with_settings :display_subprojects_issues => '1' do
276
      get :issue_report_details, :params => {:id => 1, :detail => 'tracker', :format => 'csv'}
277
      assert_response :forbidden
278
    end
279

  
280
    with_settings :display_subprojects_issues => '0' do
281
      get :issue_report_details, :params => {:id => 1, :detail => 'tracker', :format => 'csv'}
282
      assert_response :success
283
    end
284
  end
285

  
286
  def test_issue_report_details_should_not_show_csv_link_without_export_issues_permission
287
    @request.session[:user_id] = 2
288
    Role.find_by_name('Manager').remove_permission! :export_issues
289
    get :issue_report_details, :params => {:id => 1, :detail => 'tracker'}
290
    assert_response :success
291
    assert_select 'p.other-formats', 0
292
    assert_select '#csv-export-options', 0
293
  end
294

  
263 295
  def test_issue_report_details_with_tracker_detail_should_csv_export
264 296
    project = Project.find(1)
265 297
    tracker = project.trackers.find_by(:name => 'Support request')
test/functional/timelog_controller_test.rb
1728 1728
    end
1729 1729
  end
1730
  def test_index_should_not_include_csv_export_without_export_time_entries_permission
1731
    @request.session[:user_id] = 2
1732
    Role.find_by_name('Manager').remove_permission! :export_time_entries
1733
    get :index, :params => {:project_id => 1}
1734
    assert_response :success
1735

  
1736
    assert_select 'p.other-formats a.csv', 0
1737
    assert_select 'p.other-formats a.atom'
1738
    assert_select '#csv-export-options', 0
1739
  end
1740

  
1741
  def test_index_csv_should_require_export_time_entries_permission_on_every_project_of_the_entries
1742
    # User 2 is a non member of the public subproject 3 which has visible time entries
1743
    @request.session[:user_id] = 2
1744
    Role.non_member.remove_permission! :export_time_entries
1745

  
1746
    get :index, :params => {:format => 'csv'}
1747
    assert_response :forbidden
1748

  
1749
    with_settings :display_subprojects_issues => '0' do
1750
      get :index, :params => {:project_id => 1, :format => 'csv'}
1751
      assert_response :success
1752
    end
1753
  end
1754

  
1755
  def test_index_csv_without_export_time_entries_permission_should_be_denied
1756
    @request.session[:user_id] = 2
1757
    Role.find_by_name('Manager').remove_permission! :export_time_entries
1758
    get :index, :params => {:project_id => 1, :format => 'csv'}
1759
    assert_response :forbidden
1760
  end
1761

  
1762
  def test_report_should_not_include_csv_export_without_export_time_entries_permission
1763
    @request.session[:user_id] = 2
1764
    Role.find_by_name('Manager').remove_permission! :export_time_entries
1765
    get :report, :params => {:project_id => 1, :columns => 'month', :criteria => ['project']}
1766
    assert_response :success
1767

  
1768
    assert_select 'p.other-formats', 0
1769
    assert_select '#csv-export-options', 0
1770
  end
1771

  
1772
  def test_report_csv_without_export_time_entries_permission_should_be_denied
1773
    @request.session[:user_id] = 2
1774
    Role.find_by_name('Manager').remove_permission! :export_time_entries
1775
    get :report, :params => {:project_id => 1, :columns => 'month', :criteria => ['project'], :format => 'csv'}
1776
    assert_response :forbidden
1777
  end
1778

  
1730 1779
  def test_index_csv_all_projects
1731 1780
    with_settings :date_format => '%m/%d/%Y' do
1732 1781
      get :index, :params => {:format => 'csv'}
    (1-1/1)