##// END OF EJS Templates
Adds a role setting for controlling visibility of users: all or members of visible projects (#11724)....
Jean-Philippe Lang -
r13202:bdd3ccf8e52c
parent child
Show More
@@ -0,0 +1,9
1 class AddRolesUsersVisibility < ActiveRecord::Migration
2 def self.up
3 add_column :roles, :users_visibility, :string, :limit => 30, :default => 'all', :null => false
4 end
5
6 def self.down
7 remove_column :roles, :users_visibility
8 end
9 end
@@ -60,19 +60,17 class UsersController < ApplicationController
60 end
60 end
61
61
62 def show
62 def show
63 unless @user.visible?
64 render_404
65 return
66 end
67
63 # show projects based on current user visibility
68 # show projects based on current user visibility
64 @memberships = @user.memberships.where(Project.visible_condition(User.current)).to_a
69 @memberships = @user.memberships.where(Project.visible_condition(User.current)).to_a
65
70
66 events = Redmine::Activity::Fetcher.new(User.current, :author => @user).events(nil, nil, :limit => 10)
71 events = Redmine::Activity::Fetcher.new(User.current, :author => @user).events(nil, nil, :limit => 10)
67 @events_by_day = events.group_by(&:event_date)
72 @events_by_day = events.group_by(&:event_date)
68
73
69 unless User.current.admin?
70 if !@user.active? || (@user != User.current && @memberships.empty? && events.empty?)
71 render_404
72 return
73 end
74 end
75
76 respond_to do |format|
74 respond_to do |format|
77 format.html { render :layout => 'base' }
75 format.html { render :layout => 'base' }
78 format.api
76 format.api
@@ -40,8 +40,9 class WatchersController < ApplicationController
40 else
40 else
41 user_ids << params[:user_id]
41 user_ids << params[:user_id]
42 end
42 end
43 user_ids.flatten.compact.uniq.each do |user_id|
43 users = User.active.visible.where(:id => user_ids.flatten.compact.uniq)
44 Watcher.create(:watchable => @watched, :user_id => user_id)
44 users.each do |user|
45 Watcher.create(:watchable => @watched, :user => user)
45 end
46 end
46 respond_to do |format|
47 respond_to do |format|
47 format.html { redirect_to_referer_or {render :text => 'Watcher added.', :layout => true}}
48 format.html { redirect_to_referer_or {render :text => 'Watcher added.', :layout => true}}
@@ -53,7 +54,7 class WatchersController < ApplicationController
53 def append
54 def append
54 if params[:watcher].is_a?(Hash)
55 if params[:watcher].is_a?(Hash)
55 user_ids = params[:watcher][:user_ids] || [params[:watcher][:user_id]]
56 user_ids = params[:watcher][:user_ids] || [params[:watcher][:user_id]]
56 @users = User.active.where(:id => user_ids).to_a
57 @users = User.active.visible.where(:id => user_ids).to_a
57 end
58 end
58 if @users.blank?
59 if @users.blank?
59 render :nothing => true
60 render :nothing => true
@@ -61,7 +62,7 class WatchersController < ApplicationController
61 end
62 end
62
63
63 def destroy
64 def destroy
64 @watched.set_watcher(User.find(params[:user_id]), false)
65 @watched.set_watcher(User.visible.find(params[:user_id]), false)
65 respond_to do |format|
66 respond_to do |format|
66 format.html { redirect_to :back }
67 format.html { redirect_to :back }
67 format.js
68 format.js
@@ -115,12 +116,13 class WatchersController < ApplicationController
115 end
116 end
116
117
117 def users_for_new_watcher
118 def users_for_new_watcher
118 users = []
119 scope = nil
119 if params[:q].blank? && @project.present?
120 if params[:q].blank? && @project.present?
120 users = @project.users.sorted
121 scope = @project.users
121 else
122 else
122 users = User.active.sorted.like(params[:q]).limit(100)
123 scope = User.all.limit(100)
123 end
124 end
125 users = scope.active.visible.sorted.like(params[:q]).to_a
124 if @watched
126 if @watched
125 users -= @watched.watcher_users
127 users -= @watched.watcher_users
126 end
128 end
@@ -131,17 +131,17 class IssueQuery < Query
131 issue_custom_fields = []
131 issue_custom_fields = []
132
132
133 if project
133 if project
134 principals += project.principals.sort
134 principals += project.principals.visible
135 unless project.leaf?
135 unless project.leaf?
136 subprojects = project.descendants.visible.to_a
136 subprojects = project.descendants.visible.to_a
137 principals += Principal.member_of(subprojects)
137 principals += Principal.member_of(subprojects).visible
138 end
138 end
139 versions = project.shared_versions.to_a
139 versions = project.shared_versions.to_a
140 categories = project.issue_categories.to_a
140 categories = project.issue_categories.to_a
141 issue_custom_fields = project.all_issue_custom_fields
141 issue_custom_fields = project.all_issue_custom_fields
142 else
142 else
143 if all_projects.any?
143 if all_projects.any?
144 principals += Principal.member_of(all_projects)
144 principals += Principal.member_of(all_projects).visible
145 end
145 end
146 versions = Version.visible.where(:sharing => 'system').to_a
146 versions = Version.visible.where(:sharing => 'system').to_a
147 issue_custom_fields = IssueCustomField.where(:is_for_all => true)
147 issue_custom_fields = IssueCustomField.where(:is_for_all => true)
@@ -185,7 +185,7 class IssueQuery < Query
185 :type => :list_optional, :values => assigned_to_values
185 :type => :list_optional, :values => assigned_to_values
186 ) unless assigned_to_values.empty?
186 ) unless assigned_to_values.empty?
187
187
188 group_values = Group.givable.collect {|g| [g.name, g.id.to_s] }
188 group_values = Group.givable.visible.collect {|g| [g.name, g.id.to_s] }
189 add_available_filter("member_of_group",
189 add_available_filter("member_of_group",
190 :type => :list_optional, :values => group_values
190 :type => :list_optional, :values => group_values
191 ) unless group_values.empty?
191 ) unless group_values.empty?
@@ -38,6 +38,30 class Principal < ActiveRecord::Base
38 # Groups and active users
38 # Groups and active users
39 scope :active, lambda { where(:status => STATUS_ACTIVE) }
39 scope :active, lambda { where(:status => STATUS_ACTIVE) }
40
40
41 scope :visible, lambda {|*args|
42 user = args.first || User.current
43
44 if user.admin?
45 all
46 else
47 view_all_active = false
48 if user.memberships.to_a.any?
49 view_all_active = user.memberships.any? {|m| m.roles.any? {|r| r.users_visibility == 'all'}}
50 else
51 view_all_active = user.builtin_role.users_visibility == 'all'
52 end
53
54 if view_all_active
55 active
56 else
57 # self and members of visible projects
58 active.where("#{table_name}.id = ? OR #{table_name}.id IN (SELECT user_id FROM #{Member.table_name} WHERE project_id IN (?))",
59 user.id, user.visible_project_ids
60 )
61 end
62 end
63 }
64
41 scope :like, lambda {|q|
65 scope :like, lambda {|q|
42 q = q.to_s
66 q = q.to_s
43 if q.blank?
67 if q.blank?
@@ -84,6 +108,10 class Principal < ActiveRecord::Base
84 to_s
108 to_s
85 end
109 end
86
110
111 def visible?(user=User.current)
112 Principal.visible(user).where(:id => id).first == self
113 end
114
87 # Return true if the principal is a member of project
115 # Return true if the principal is a member of project
88 def member_of?(project)
116 def member_of?(project)
89 projects.to_a.include?(project)
117 projects.to_a.include?(project)
@@ -39,6 +39,11 class Role < ActiveRecord::Base
39 ['own', :label_issues_visibility_own]
39 ['own', :label_issues_visibility_own]
40 ]
40 ]
41
41
42 USERS_VISIBILITY_OPTIONS = [
43 ['all', :label_users_visibility_all],
44 ['members_of_visible_projects', :label_users_visibility_members_of_visible_projects]
45 ]
46
42 scope :sorted, lambda { order(:builtin, :position) }
47 scope :sorted, lambda { order(:builtin, :position) }
43 scope :givable, lambda { order(:position).where(:builtin => 0) }
48 scope :givable, lambda { order(:position).where(:builtin => 0) }
44 scope :builtin, lambda { |*args|
49 scope :builtin, lambda { |*args|
@@ -67,6 +72,9 class Role < ActiveRecord::Base
67 validates_inclusion_of :issues_visibility,
72 validates_inclusion_of :issues_visibility,
68 :in => ISSUES_VISIBILITY_OPTIONS.collect(&:first),
73 :in => ISSUES_VISIBILITY_OPTIONS.collect(&:first),
69 :if => lambda {|role| role.respond_to?(:issues_visibility)}
74 :if => lambda {|role| role.respond_to?(:issues_visibility)}
75 validates_inclusion_of :users_visibility,
76 :in => USERS_VISIBILITY_OPTIONS.collect(&:first),
77 :if => lambda {|role| role.respond_to?(:users_visibility)}
70
78
71 # Copies attributes from another role, arg can be an id or a Role
79 # Copies attributes from another role, arg can be an id or a Role
72 def copy_from(arg, options={})
80 def copy_from(arg, options={})
@@ -40,20 +40,20 class TimeEntryQuery < Query
40
40
41 principals = []
41 principals = []
42 if project
42 if project
43 principals += project.principals.sort
43 principals += project.principals.visible.sort
44 unless project.leaf?
44 unless project.leaf?
45 subprojects = project.descendants.visible.to_a
45 subprojects = project.descendants.visible.to_a
46 if subprojects.any?
46 if subprojects.any?
47 add_available_filter "subproject_id",
47 add_available_filter "subproject_id",
48 :type => :list_subprojects,
48 :type => :list_subprojects,
49 :values => subprojects.collect{|s| [s.name, s.id.to_s] }
49 :values => subprojects.collect{|s| [s.name, s.id.to_s] }
50 principals += Principal.member_of(subprojects)
50 principals += Principal.member_of(subprojects).visible
51 end
51 end
52 end
52 end
53 else
53 else
54 if all_projects.any?
54 if all_projects.any?
55 # members of visible projects
55 # members of visible projects
56 principals += Principal.member_of(all_projects)
56 principals += Principal.member_of(all_projects).visible
57 # project filter
57 # project filter
58 project_values = []
58 project_values = []
59 if User.current.logged? && User.current.memberships.any?
59 if User.current.logged? && User.current.memberships.any?
@@ -148,6 +148,7 class User < Principal
148 @notified_projects_ids = nil
148 @notified_projects_ids = nil
149 @notified_projects_ids_changed = false
149 @notified_projects_ids_changed = false
150 @builtin_role = nil
150 @builtin_role = nil
151 @visible_project_ids = nil
151 base_reload(*args)
152 base_reload(*args)
152 end
153 end
153
154
@@ -528,6 +529,11 class User < Principal
528 @projects_by_role = hash
529 @projects_by_role = hash
529 end
530 end
530
531
532 # Returns the ids of visible projects
533 def visible_project_ids
534 @visible_project_ids ||= Project.visible(self).pluck(:id)
535 end
536
531 # Returns true if user is arg or belongs to arg
537 # Returns true if user is arg or belongs to arg
532 def is_or_belongs_to?(arg)
538 def is_or_belongs_to?(arg)
533 if arg.is_a?(User)
539 if arg.is_a?(User)
@@ -1,18 +1,22
1 <%= error_messages_for 'role' %>
1 <%= error_messages_for 'role' %>
2
2
3 <% unless @role.anonymous? %>
4 <div class="box tabular">
3 <div class="box tabular">
5 <% unless @role.builtin? %>
4 <% unless @role.builtin? %>
6 <p><%= f.text_field :name, :required => true %></p>
5 <p><%= f.text_field :name, :required => true %></p>
7 <p><%= f.check_box :assignable %></p>
6 <p><%= f.check_box :assignable %></p>
8 <% end %>
7 <% end %>
9 <p><%= f.select :issues_visibility, Role::ISSUES_VISIBILITY_OPTIONS.collect {|v| [l(v.last), v.first]} %></p>
8
10 <% if @role.new_record? && @roles.any? %>
9 <% unless @role.anonymous? %>
11 <p><label for="copy_workflow_from"><%= l(:label_copy_workflow_from) %></label>
10 <p><%= f.select :issues_visibility, Role::ISSUES_VISIBILITY_OPTIONS.collect {|v| [l(v.last), v.first]} %></p>
12 <%= select_tag(:copy_workflow_from, content_tag("option") + options_from_collection_for_select(@roles, :id, :name, params[:copy_workflow_from] || @copy_from.try(:id))) %></p>
11 <% end %>
13 <% end %>
12
13 <p><%= f.select :users_visibility, Role::USERS_VISIBILITY_OPTIONS.collect {|v| [l(v.last), v.first]} %></p>
14
15 <% if @role.new_record? && @roles.any? %>
16 <p><label for="copy_workflow_from"><%= l(:label_copy_workflow_from) %></label>
17 <%= select_tag(:copy_workflow_from, content_tag("option") + options_from_collection_for_select(@roles, :id, :name, params[:copy_workflow_from] || @copy_from.try(:id))) %></p>
18 <% end %>
14 </div>
19 </div>
15 <% end %>
16
20
17 <h3><%= l(:label_permissions) %></h3>
21 <h3><%= l(:label_permissions) %></h3>
18 <div class="box tabular" id="permissions">
22 <div class="box tabular" id="permissions">
@@ -338,6 +338,7 en:
338 field_generate_password: Generate password
338 field_generate_password: Generate password
339 field_must_change_passwd: Must change password at next logon
339 field_must_change_passwd: Must change password at next logon
340 field_default_status: Default status
340 field_default_status: Default status
341 field_users_visibility: Users visibility
341
342
342 setting_app_title: Application title
343 setting_app_title: Application title
343 setting_app_subtitle: Application subtitle
344 setting_app_subtitle: Application subtitle
@@ -920,6 +921,8 en:
920 label_latest_compatible_version: Latest compatible version
921 label_latest_compatible_version: Latest compatible version
921 label_unknown_plugin: Unknown plugin
922 label_unknown_plugin: Unknown plugin
922 label_add_projects: Add projects
923 label_add_projects: Add projects
924 label_users_visibility_all: All active users
925 label_users_visibility_members_of_visible_projects: Members of visible projects
923
926
924 button_login: Login
927 button_login: Login
925 button_submit: Submit
928 button_submit: Submit
@@ -358,6 +358,7 fr:
358 field_generate_password: GΓ©nΓ©rer un mot de passe
358 field_generate_password: GΓ©nΓ©rer un mot de passe
359 field_must_change_passwd: Doit changer de mot de passe Γ  la prochaine connexion
359 field_must_change_passwd: Doit changer de mot de passe Γ  la prochaine connexion
360 field_default_status: Statut par dΓ©faut
360 field_default_status: Statut par dΓ©faut
361 field_users_visibility: VisibilitΓ© des utilisateurs
361
362
362 setting_app_title: Titre de l'application
363 setting_app_title: Titre de l'application
363 setting_app_subtitle: Sous-titre de l'application
364 setting_app_subtitle: Sous-titre de l'application
@@ -940,6 +941,8 fr:
940 label_latest_compatible_version: Dernière version compatible
941 label_latest_compatible_version: Dernière version compatible
941 label_unknown_plugin: Plugin inconnu
942 label_unknown_plugin: Plugin inconnu
942 label_add_projects: Ajouter des projets
943 label_add_projects: Ajouter des projets
944 label_users_visibility_all: Tous les utilisateurs actifs
945 label_users_visibility_members_of_visible_projects: Membres des projets visibles
943
946
944 button_login: Connexion
947 button_login: Connexion
945 button_submit: Soumettre
948 button_submit: Soumettre
@@ -42,6 +42,7 module Redmine
42 # Roles
42 # Roles
43 manager = Role.create! :name => l(:default_role_manager),
43 manager = Role.create! :name => l(:default_role_manager),
44 :issues_visibility => 'all',
44 :issues_visibility => 'all',
45 :users_visibility => 'all',
45 :position => 1
46 :position => 1
46 manager.permissions = manager.setable_permissions.collect {|p| p.name}
47 manager.permissions = manager.setable_permissions.collect {|p| p.name}
47 manager.save!
48 manager.save!
@@ -4,6 +4,7 roles_001:
4 id: 1
4 id: 1
5 builtin: 0
5 builtin: 0
6 issues_visibility: all
6 issues_visibility: all
7 users_visibility: all
7 permissions: |
8 permissions: |
8 ---
9 ---
9 - :add_project
10 - :add_project
@@ -67,6 +68,7 roles_002:
67 id: 2
68 id: 2
68 builtin: 0
69 builtin: 0
69 issues_visibility: default
70 issues_visibility: default
71 users_visibility: all
70 permissions: |
72 permissions: |
71 ---
73 ---
72 - :edit_project
74 - :edit_project
@@ -114,6 +116,7 roles_003:
114 id: 3
116 id: 3
115 builtin: 0
117 builtin: 0
116 issues_visibility: default
118 issues_visibility: default
119 users_visibility: all
117 permissions: |
120 permissions: |
118 ---
121 ---
119 - :edit_project
122 - :edit_project
@@ -155,6 +158,7 roles_004:
155 id: 4
158 id: 4
156 builtin: 1
159 builtin: 1
157 issues_visibility: default
160 issues_visibility: default
161 users_visibility: all
158 permissions: |
162 permissions: |
159 ---
163 ---
160 - :view_issues
164 - :view_issues
@@ -184,6 +188,7 roles_005:
184 id: 5
188 id: 5
185 builtin: 2
189 builtin: 2
186 issues_visibility: default
190 issues_visibility: default
191 users_visibility: all
187 permissions: |
192 permissions: |
188 ---
193 ---
189 - :view_issues
194 - :view_issues
@@ -106,12 +106,6 class UsersControllerTest < ActionController::TestCase
106 assert_response 404
106 assert_response 404
107 end
107 end
108
108
109 def test_show_should_not_reveal_users_with_no_visible_activity_or_project
110 @request.session[:user_id] = nil
111 get :show, :id => 9
112 assert_response 404
113 end
114
115 def test_show_inactive_by_admin
109 def test_show_inactive_by_admin
116 @request.session[:user_id] = 1
110 @request.session[:user_id] = 1
117 get :show, :id => 5
111 get :show, :id => 5
@@ -119,6 +113,15 class UsersControllerTest < ActionController::TestCase
119 assert_not_nil assigns(:user)
113 assert_not_nil assigns(:user)
120 end
114 end
121
115
116 def test_show_user_who_is_not_visible_should_return_404
117 Role.anonymous.update! :users_visibility => 'members_of_visible_projects'
118 user = User.generate!
119
120 @request.session[:user_id] = nil
121 get :show, :id => user.id
122 assert_response 404
123 end
124
122 def test_show_displays_memberships_based_on_project_visibility
125 def test_show_displays_memberships_based_on_project_visibility
123 @request.session[:user_id] = 1
126 @request.session[:user_id] = 1
124 get :show, :id => 2
127 get :show, :id => 2
@@ -227,6 +227,21 class WatchersControllerTest < ActionController::TestCase
227 assert Issue.find(2).watched_by?(user)
227 assert Issue.find(2).watched_by?(user)
228 end
228 end
229
229
230 def test_autocomplete_for_user_should_return_visible_users
231 Role.update_all :users_visibility => 'members_of_visible_projects'
232
233 hidden = User.generate!(:lastname => 'autocomplete')
234 visible = User.generate!(:lastname => 'autocomplete')
235 User.add_to_project(visible, Project.find(1))
236
237 @request.session[:user_id] = 2
238 xhr :get, :autocomplete_for_user, :q => 'autocomp', :project_id => 'ecookbook'
239 assert_response :success
240
241 assert_include visible, assigns(:users)
242 assert_not_include hidden, assigns(:users)
243 end
244
230 def test_append
245 def test_append
231 @request.session[:user_id] = 2
246 @request.session[:user_id] = 2
232 assert_no_difference 'Watcher.count' do
247 assert_no_difference 'Watcher.count' do
@@ -20,7 +20,7
20 require File.expand_path('../../test_helper', __FILE__)
20 require File.expand_path('../../test_helper', __FILE__)
21
21
22 class PrincipalTest < ActiveSupport::TestCase
22 class PrincipalTest < ActiveSupport::TestCase
23 fixtures :users, :projects, :members, :member_roles
23 fixtures :users, :projects, :members, :member_roles, :roles
24
24
25 def test_active_scope_should_return_groups_and_active_users
25 def test_active_scope_should_return_groups_and_active_users
26 result = Principal.active.to_a
26 result = Principal.active.to_a
@@ -30,6 +30,27 class PrincipalTest < ActiveSupport::TestCase
30 assert_nil result.detect {|p| p.is_a?(AnonymousUser)}
30 assert_nil result.detect {|p| p.is_a?(AnonymousUser)}
31 end
31 end
32
32
33 def test_visible_scope_for_admin_should_return_all_principals
34 admin = User.generate! {|u| u.admin = true}
35 assert_equal Principal.count, Principal.visible(admin).count
36 end
37
38 def test_visible_scope_for_user_with_members_of_visible_projects_visibility_should_return_active_principals
39 Role.non_member.update! :users_visibility => 'all'
40 user = User.generate!
41
42 expected = Principal.active
43 assert_equal expected.map(&:id).sort, Principal.visible(user).pluck(:id).sort
44 end
45
46 def test_visible_scope_for_user_with_members_of_visible_projects_visibility_should_return_members_of_visible_projects_and_self
47 Role.non_member.update! :users_visibility => 'members_of_visible_projects'
48 user = User.generate!
49
50 expected = Project.visible(user).map(&:member_principals).flatten.map(&:principal).uniq << user
51 assert_equal expected.map(&:id).sort, Principal.visible(user).pluck(:id).sort
52 end
53
33 def test_member_of_scope_should_return_the_union_of_all_members
54 def test_member_of_scope_should_return_the_union_of_all_members
34 projects = Project.find([1])
55 projects = Project.find([1])
35 assert_equal [3, 2], Principal.member_of(projects).sort.map(&:id)
56 assert_equal [3, 2], Principal.member_of(projects).sort.map(&:id)
General Comments 0
You need to be logged in to leave comments. Login now