From 6e68d008c4dc50f46585e30aabaa5bc9859bee0f Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Mon, 6 Jun 2016 09:41:50 +0000 Subject: Merged 15430, 15464 to 15469, 15475, 15476 (#285, #7839). git-svn-id: http://svn.redmine.org/redmine/branches/3.3-stable@15478 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/context_menus_controller.rb | 7 +-- app/controllers/issues_controller.rb | 21 ++++++- app/helpers/issues_helper.rb | 10 +++ app/models/issue.rb | 98 ++++++++++++++++++++++------- app/models/issue_import.rb | 2 +- app/models/mail_handler.rb | 13 ++-- app/models/role.rb | 51 +++++++++++++++ app/views/issues/_action_menu.html.erb | 2 +- app/views/issues/_edit.html.erb | 29 ++++----- app/views/issues/_form.html.erb | 2 +- app/views/issues/_history.html.erb | 2 +- app/views/issues/index.html.erb | 2 +- app/views/issues/show.html.erb | 2 +- app/views/roles/_form.html.erb | 61 +++++++++++++++--- 14 files changed, 238 insertions(+), 64 deletions(-) (limited to 'app') diff --git a/app/controllers/context_menus_controller.rb b/app/controllers/context_menus_controller.rb index 1e5652d09..66ec35085 100644 --- a/app/controllers/context_menus_controller.rb +++ b/app/controllers/context_menus_controller.rb @@ -29,11 +29,11 @@ class ContextMenusController < ApplicationController @allowed_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&) - @can = {:edit => User.current.allowed_to?(:edit_issues, @projects), + @can = {:edit => @issues.all?(&:attributes_editable?), :log_time => (@project && User.current.allowed_to?(:log_time, @project)), :copy => User.current.allowed_to?(:copy_issues, @projects) && Issue.allowed_target_projects.any?, :add_watchers => User.current.allowed_to?(:add_issue_watchers, @projects), - :delete => User.current.allowed_to?(:delete_issues, @projects) + :delete => @issues.all?(&:deletable?) } if @project if @issue @@ -41,12 +41,11 @@ class ContextMenusController < ApplicationController else @assignables = @project.assignable_users end - @trackers = @project.trackers else #when multiple projects, we only keep the intersection of each set @assignables = @projects.map(&:assignable_users).reduce(:&) - @trackers = @projects.map(&:trackers).reduce(:&) end + @trackers = @projects.map {|p| Issue.allowed_target_trackers(p) }.reduce(:&) @versions = @projects.map {|p| p.shared_versions.open}.reduce(:&) @priorities = IssuePriority.active.reverse diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb index cf402de22..67956667a 100644 --- a/app/controllers/issues_controller.rb +++ b/app/controllers/issues_controller.rb @@ -211,6 +211,10 @@ class IssuesController < ApplicationController unless User.current.allowed_to?(:copy_issues, @projects) raise ::Unauthorized end + else + unless @issues.all?(&:attributes_editable?) + raise ::Unauthorized + end end @allowed_projects = Issue.allowed_target_projects @@ -230,7 +234,7 @@ class IssuesController < ApplicationController end @custom_fields = @issues.map{|i|i.editable_custom_fields}.reduce(:&) @assignables = target_projects.map(&:assignable_users).reduce(:&) - @trackers = target_projects.map(&:trackers).reduce(:&) + @trackers = target_projects.map {|p| Issue.allowed_target_trackers(p) }.reduce(:&) @versions = target_projects.map {|p| p.shared_versions.open}.reduce(:&) @categories = target_projects.map {|p| p.issue_categories}.reduce(:&) if @copy @@ -263,6 +267,10 @@ class IssuesController < ApplicationController unless User.current.allowed_to?(:add_issues, target_projects) raise ::Unauthorized end + else + unless @issues.all?(&:attributes_editable?) + raise ::Unauthorized + end end unsaved_issues = [] @@ -316,6 +324,7 @@ class IssuesController < ApplicationController end def destroy + raise Unauthorized unless @issues.all?(&:deletable?) @hours = TimeEntry.where(:issue_id => @issues.map(&:id)).sum(:hours).to_f if @hours > 0 case params[:todo] @@ -465,9 +474,15 @@ class IssuesController < ApplicationController @issue.safe_attributes = attrs if @issue.project - @issue.tracker ||= @issue.project.trackers.first + @issue.tracker ||= @issue.allowed_target_trackers.first if @issue.tracker.nil? - render_error l(:error_no_tracker_in_project) + if @issue.project.trackers.any? + # None of the project trackers is allowed to the user + render_error :message => l(:error_no_tracker_allowed_for_new_issue_in_project), :status => 403 + else + # Project has no trackers + render_error l(:error_no_tracker_in_project) + end return false end if @issue.status.nil? diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index b505765d8..ce6d9f8f2 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -168,6 +168,16 @@ module IssuesHelper link_to(l(:button_add), new_project_issue_path(issue.project, :issue => attrs)) end + def trackers_options_for_select(issue) + trackers = issue.allowed_target_trackers + if issue.new_record? && issue.parent_issue_id.present? + trackers = trackers.reject do |tracker| + issue.tracker_id != tracker.id && tracker.disabled_core_fields.include?('parent_issue_id') + end + end + trackers.collect {|t| [t.name, t.id]} + end + class IssueFieldsRows include ActionView::Helpers::TagHelper diff --git a/app/models/issue.rb b/app/models/issue.rb index 0ee8ff3d3..e0b99e4e6 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -120,10 +120,10 @@ class Issue < ActiveRecord::Base # Returns a SQL conditions string used to find all issues visible by the specified user def self.visible_condition(user, options={}) Project.allowed_to_condition(user, :view_issues, options) do |role, user| - if user.id && user.logged? + sql = if user.id && user.logged? case role.issues_visibility when 'all' - nil + '1=1' when 'default' user_ids = [user.id] + user.groups.map(&:id).compact "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))" @@ -136,13 +136,22 @@ class Issue < ActiveRecord::Base else "(#{table_name}.is_private = #{connection.quoted_false})" end + unless role.permissions_all_trackers?(:view_issues) + tracker_ids = role.permissions_tracker_ids(:view_issues) + if tracker_ids.any? + sql = "(#{sql} AND #{table_name}.tracker_id IN (#{tracker_ids.join(',')}))" + else + sql = '1=0' + end + end + sql end end # Returns true if usr or current user is allowed to view the issue def visible?(usr=nil) (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user| - if user.logged? + visible = if user.logged? case role.issues_visibility when 'all' true @@ -156,17 +165,36 @@ class Issue < ActiveRecord::Base else !self.is_private? end + unless role.permissions_all_trackers?(:view_issues) + visible &&= role.permissions_tracker_ids?(:view_issues, tracker_id) + end + visible end end - # Returns true if user or current user is allowed to edit or add a note to the issue + # Returns true if user or current user is allowed to edit or add notes to the issue def editable?(user=User.current) - attributes_editable?(user) || user.allowed_to?(:add_issue_notes, project) + attributes_editable?(user) || notes_addable?(user) end # Returns true if user or current user is allowed to edit the issue def attributes_editable?(user=User.current) - user.allowed_to?(:edit_issues, project) + user_tracker_permission?(user, :edit_issues) + end + + # Overrides Redmine::Acts::Attachable::InstanceMethods#attachments_editable? + def attachments_editable?(user=User.current) + attributes_editable?(user) + end + + # Returns true if user or current user is allowed to add notes to the issue + def notes_addable?(user=User.current) + user_tracker_permission?(user, :add_issue_notes) + end + + # Returns true if user or current user is allowed to delete the issue + def deletable?(user=User.current) + user_tracker_permission?(user, :delete_issues) end def initialize(attributes=nil, *args) @@ -416,10 +444,10 @@ class Issue < ActiveRecord::Base 'custom_fields', 'lock_version', 'notes', - :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) } + :if => lambda {|issue, user| issue.new_record? || issue.attributes_editable?(user) } safe_attributes 'notes', - :if => lambda {|issue, user| user.allowed_to?(:add_issue_notes, issue.project)} + :if => lambda {|issue, user| issue.notes_addable?(user)} safe_attributes 'private_notes', :if => lambda {|issue, user| !issue.new_record? && user.allowed_to?(:set_notes_private, issue.project)} @@ -434,7 +462,7 @@ class Issue < ActiveRecord::Base } safe_attributes 'parent_issue_id', - :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) && + :if => lambda {|issue, user| (issue.new_record? || issue.attributes_editable?(user)) && user.allowed_to?(:manage_subtasks, issue.project)} def safe_attribute_names(user=nil) @@ -479,12 +507,14 @@ class Issue < ActiveRecord::Base end if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id') - self.tracker_id = t + if allowed_target_trackers(user).where(:id => t.to_i).exists? + self.tracker_id = t + end end if project - # Set the default tracker to accept custom field values + # Set a default tracker to accept custom field values # even if tracker is not specified - self.tracker ||= project.trackers.first + self.tracker ||= allowed_target_trackers(user).first end statuses_allowed = new_statuses_allowed_to(user) @@ -822,16 +852,6 @@ class Issue < ActiveRecord::Base !leaf? end - def assignable_trackers - trackers = project.trackers - if new_record? && parent_issue_id.present? - trackers = trackers.reject do |tracker| - tracker_id != tracker.id && tracker.disabled_core_fields.include?('parent_issue_id') - end - end - trackers - end - # Users the issue can be assigned to def assignable_users users = project.assignable_users.to_a @@ -1373,9 +1393,43 @@ class Issue < ActiveRecord::Base end Project.where(condition).having_trackers end + + # Returns a scope of trackers that user can assign the issue to + def allowed_target_trackers(user=User.current) + self.class.allowed_target_trackers(project, user, tracker_id_was) + end + + # Returns a scope of trackers that user can assign project issues to + def self.allowed_target_trackers(project, user=User.current, current_tracker=nil) + if project + scope = project.trackers.sorted + unless user.admin? + roles = user.roles_for_project(project).select {|r| r.has_permission?(:add_issues)} + unless roles.any? {|r| r.permissions_all_trackers?(:add_issues)} + tracker_ids = roles.map {|r| r.permissions_tracker_ids(:add_issues)}.flatten.uniq + if current_tracker + tracker_ids << current_tracker + end + scope = scope.where(:id => tracker_ids) + end + end + scope + else + Tracker.none + end + end private + def user_tracker_permission?(user, permission) + if user.admin? + true + else + roles = user.roles_for_project(project).select {|r| r.has_permission?(permission)} + roles.any? {|r| r.permissions_all_trackers?(permission) || r.permissions_tracker_ids?(permission, tracker_id)} + end + end + def after_project_change # Update project_id on related time entries TimeEntry.where({:issue_id => id}).update_all(["project_id = ?", project_id]) diff --git a/app/models/issue_import.rb b/app/models/issue_import.rb index b6b20a1b1..5b19ac966 100644 --- a/app/models/issue_import.rb +++ b/app/models/issue_import.rb @@ -37,7 +37,7 @@ class IssueImport < Import # Returns a scope of trackers that user is allowed to # import issue to def allowed_target_trackers - project.trackers + Issue.allowed_target_trackers(project, user) end def tracker diff --git a/app/models/mail_handler.rb b/app/models/mail_handler.rb index 40bd02a07..a619f115d 100644 --- a/app/models/mail_handler.rb +++ b/app/models/mail_handler.rb @@ -199,7 +199,14 @@ class MailHandler < ActionMailer::Base end issue = Issue.new(:author => user, :project => project) - issue.safe_attributes = issue_attributes_from_keywords(issue) + attributes = issue_attributes_from_keywords(issue) + if handler_options[:no_permission_check] + issue.tracker_id = attributes['tracker_id'] + if project + issue.tracker_id ||= project.trackers.first.try(:id) + end + end + issue.safe_attributes = attributes issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)} issue.subject = cleaned_up_subject if issue.subject.blank? @@ -420,10 +427,6 @@ class MailHandler < ActionMailer::Base 'done_ratio' => get_keyword(:done_ratio, :format => '(\d|10)?0') }.delete_if {|k, v| v.blank? } - if issue.new_record? && attrs['tracker_id'].nil? - attrs['tracker_id'] = issue.project.trackers.first.try(:id) - end - attrs end diff --git a/app/models/role.rb b/app/models/role.rb index defbc311d..89538aa4d 100644 --- a/app/models/role.rb +++ b/app/models/role.rb @@ -73,6 +73,7 @@ class Role < ActiveRecord::Base acts_as_positioned :scope => :builtin serialize :permissions, ::Role::PermissionsAttributeCoder + store :settings, :accessors => [:permissions_all_trackers, :permissions_tracker_ids] attr_protected :builtin validates_presence_of :name @@ -188,6 +189,56 @@ class Role < ActiveRecord::Base setable_permissions end + def permissions_tracker_ids(*args) + if args.any? + Array(permissions_tracker_ids[args.first.to_s]).map(&:to_i) + else + super || {} + end + end + + def permissions_tracker_ids=(arg) + h = arg.to_hash + h.values.each {|v| v.reject!(&:blank?)} + super(h) + end + + # Returns true if tracker_id belongs to the list of + # trackers for which permission is given + def permissions_tracker_ids?(permission, tracker_id) + permissions_tracker_ids(permission).include?(tracker_id) + end + + def permissions_all_trackers + super || {} + end + + def permissions_all_trackers=(arg) + super(arg.to_hash) + end + + # Returns true if permission is given for all trackers + def permissions_all_trackers?(permission) + permissions_all_trackers[permission.to_s].to_s != '0' + end + + # Sets the trackers that are allowed for a permission. + # tracker_ids can be an array of tracker ids or :all for + # no restrictions. + # + # Examples: + # role.set_permission_trackers :add_issues, [1, 3] + # role.set_permission_trackers :add_issues, :all + def set_permission_trackers(permission, tracker_ids) + h = {permission.to_s => (tracker_ids == :all ? '1' : '0')} + self.permissions_all_trackers = permissions_all_trackers.merge(h) + + h = {permission.to_s => (tracker_ids == :all ? [] : tracker_ids)} + self.permissions_tracker_ids = permissions_tracker_ids.merge(h) + + self + end + # Find all the roles that can be given to a project member def self.find_all_givable Role.givable.to_a diff --git a/app/views/issues/_action_menu.html.erb b/app/views/issues/_action_menu.html.erb index c3304626d..b535faec9 100644 --- a/app/views/issues/_action_menu.html.erb +++ b/app/views/issues/_action_menu.html.erb @@ -3,5 +3,5 @@ <%= link_to l(:button_log_time), new_issue_time_entry_path(@issue), :class => 'icon icon-time-add' if User.current.allowed_to?(:log_time, @project) %> <%= watcher_link(@issue, User.current) %> <%= link_to l(:button_copy), project_copy_issue_path(@project, @issue), :class => 'icon icon-copy' if User.current.allowed_to?(:copy_issues, @project) && Issue.allowed_target_projects.any? %> -<%= link_to l(:button_delete), issue_path(@issue), :data => {:confirm => issues_destroy_confirmation_message(@issue)}, :method => :delete, :class => 'icon icon-del' if User.current.allowed_to?(:delete_issues, @project) %> +<%= link_to l(:button_delete), issue_path(@issue), :data => {:confirm => issues_destroy_confirmation_message(@issue)}, :method => :delete, :class => 'icon icon-del' if @issue.deletable? %> diff --git a/app/views/issues/_edit.html.erb b/app/views/issues/_edit.html.erb index 24a01cad0..67e33246d 100644 --- a/app/views/issues/_edit.html.erb +++ b/app/views/issues/_edit.html.erb @@ -27,21 +27,22 @@ <% end %> <% end %> - -
<%= l(:field_notes) %> - <%= f.text_area :notes, :cols => 60, :rows => 10, :class => 'wiki-edit', :no_label => true %> - <%= wikitoolbar_for 'issue_notes' %> - - <% if @issue.safe_attribute? 'private_notes' %> - <%= f.check_box :private_notes, :no_label => true %> + <% if @issue.notes_addable? %> +
<%= l(:field_notes) %> + <%= f.text_area :notes, :cols => 60, :rows => 10, :class => 'wiki-edit', :no_label => true %> + <%= wikitoolbar_for 'issue_notes' %> + + <% if @issue.safe_attribute? 'private_notes' %> + <%= f.check_box :private_notes, :no_label => true %> + <% end %> + + <%= call_hook(:view_issues_edit_notes_bottom, { :issue => @issue, :notes => @notes, :form => f }) %> +
+ +
<%= l(:label_attachment_plural) %> +

<%= render :partial => 'attachments/form', :locals => {:container => @issue} %>

+
<% end %> - - <%= call_hook(:view_issues_edit_notes_bottom, { :issue => @issue, :notes => @notes, :form => f }) %> -
- -
<%= l(:label_attachment_plural) %> -

<%= render :partial => 'attachments/form', :locals => {:container => @issue} %>

-
<%= f.hidden_field :lock_version %> diff --git a/app/views/issues/_form.html.erb b/app/views/issues/_form.html.erb index 788a2f38e..1e82b6f84 100644 --- a/app/views/issues/_form.html.erb +++ b/app/views/issues/_form.html.erb @@ -14,7 +14,7 @@ <% end %> <% if @issue.safe_attribute? 'tracker_id' %> -

<%= f.select :tracker_id, @issue.assignable_trackers.collect {|t| [t.name, t.id]}, {:required => true}, +

<%= f.select :tracker_id, trackers_options_for_select(@issue), {:required => true}, :onchange => "updateIssueFrom('#{escape_javascript update_issue_form_path(@project, @issue)}', this)" %>

<% end %> diff --git a/app/views/issues/_history.html.erb b/app/views/issues/_history.html.erb index 993dfee3c..307ac909b 100644 --- a/app/views/issues/_history.html.erb +++ b/app/views/issues/_history.html.erb @@ -1,4 +1,4 @@ -<% reply_links = authorize_for('issues', 'edit') -%> +<% reply_links = issue.notes_addable? -%> <% for journal in journals %>
diff --git a/app/views/issues/index.html.erb b/app/views/issues/index.html.erb index 7dcba2629..fd762023f 100644 --- a/app/views/issues/index.html.erb +++ b/app/views/issues/index.html.erb @@ -1,5 +1,5 @@
- <% if User.current.allowed_to?(:add_issues, @project, :global => true) && (@project.nil? || @project.trackers.any?) %> + <% if User.current.allowed_to?(:add_issues, @project, :global => true) && (@project.nil? || Issue.allowed_target_trackers(@project).any?) %> <%= link_to l(:label_issue_new), _new_project_issue_path(@project), :class => 'icon icon-add new-issue' %> <% end %>
diff --git a/app/views/issues/show.html.erb b/app/views/issues/show.html.erb index 70a7fe165..2cbff32e5 100644 --- a/app/views/issues/show.html.erb +++ b/app/views/issues/show.html.erb @@ -77,7 +77,7 @@ end %> <% if @issue.description? %>
- <%= link_to l(:button_quote), quoted_issue_path(@issue), :remote => true, :method => 'post', :class => 'icon icon-comment' if authorize_for('issues', 'edit') %> + <%= link_to l(:button_quote), quoted_issue_path(@issue), :remote => true, :method => 'post', :class => 'icon icon-comment' if @issue.notes_addable? %>

<%=l(:field_description)%>

diff --git a/app/views/roles/_form.html.erb b/app/views/roles/_form.html.erb index 84d5de185..39acc34a4 100644 --- a/app/views/roles/_form.html.erb +++ b/app/views/roles/_form.html.erb @@ -7,17 +7,17 @@ <% end %> <% unless @role.anonymous? %> -

<%= f.select :issues_visibility, Role::ISSUES_VISIBILITY_OPTIONS.collect {|v| [l(v.last), v.first]} %>

+

<%= f.select :issues_visibility, Role::ISSUES_VISIBILITY_OPTIONS.collect {|v| [l(v.last), v.first]} %>

<% end %> <% unless @role.anonymous? %> -

<%= f.select :time_entries_visibility, Role::TIME_ENTRIES_VISIBILITY_OPTIONS.collect {|v| [l(v.last), v.first]} %>

+

<%= f.select :time_entries_visibility, Role::TIME_ENTRIES_VISIBILITY_OPTIONS.collect {|v| [l(v.last), v.first]} %>

<% end %>

<%= f.select :users_visibility, Role::USERS_VISIBILITY_OPTIONS.collect {|v| [l(v.last), v.first]} %>

<% unless @role.builtin? %> -

+

-<%= javascript_tag do %> -$(document).ready(function(){ - $("#role_permissions_manage_members").change(function(){ - $("#manage_members_options").toggle($(this).is(":checked")); - }).change(); -}); +
+

<%= l(:label_issue_tracking) %>

+<% permissions = %w(view_issues add_issues edit_issues add_issue_notes delete_issues) %> + +
+ + + + + <% permissions.each do |permission| %> + + <% end %> + + + + + <% permissions.each do |permission| %> + + <% end %> + + <% Tracker.sorted.all.each do |tracker| %> + "> + + <% permissions.each do |permission| %> + + <% end %> + + <% end %> + +
<%= l(:label_tracker) %>"><%= l("permission_#{permission}") %>
<%= l(:label_tracker_all) %>"> + <%= hidden_field_tag "role[permissions_all_trackers][#{permission}]", '0', :id => nil %> + <%= check_box_tag "role[permissions_all_trackers][#{permission}]", + '1', + @role.permissions_all_trackers?(permission), + :class => "#{permission}_shown", + :data => {:disables => ".#{permission}_tracker"} %> +
<%= tracker.name %>"><%= check_box_tag "role[permissions_tracker_ids][#{permission}][]", + tracker.id, + @role.permissions_tracker_ids?(permission, tracker.id), + :class => "#{permission}_tracker", + :id => "role_permissions_tracker_ids_add_issues_#{tracker.id}" %>
+
+ +<% permissions.each do |permission| %> + <%= hidden_field_tag "role[permissions_tracker_ids][#{permission}][]", '' %> <% end %> +
-- cgit v1.2.3