##// END OF EJS Templates
Merged r16049 (#24156)....
Jean-Philippe Lang -
r15711:d5a943fd132c
parent child
Show More
@@ -1,56 +1,56
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class GroupBuiltin < Group
18 class GroupBuiltin < Group
19 validate :validate_uniqueness, :on => :create
19 validate :validate_uniqueness, :on => :create
20
20
21 def validate_uniqueness
21 def validate_uniqueness
22 errors.add :base, 'The builtin group already exists.' if self.class.exists?
22 errors.add :base, 'The builtin group already exists.' if self.class.exists?
23 end
23 end
24
24
25 def builtin?
25 def builtin?
26 true
26 true
27 end
27 end
28
28
29 def destroy
29 def destroy
30 false
30 false
31 end
31 end
32
32
33 def user_added(user)
33 def user_added(user)
34 raise 'Cannot add users to a builtin group'
34 raise 'Cannot add users to a builtin group'
35 end
35 end
36
36
37 class << self
37 class << self
38 def load_instance
38 def load_instance
39 return nil if self == GroupBuiltin
39 return nil if self == GroupBuiltin
40 instance = order('id').first || create_instance
40 instance = unscoped.order('id').first || create_instance
41 end
41 end
42
42
43 def create_instance
43 def create_instance
44 raise 'The builtin group already exists.' if exists?
44 raise 'The builtin group already exists.' if exists?
45 instance = new
45 instance = unscoped.new
46 instance.lastname = name
46 instance.lastname = name
47 instance.save :validate => false
47 instance.save :validate => false
48 raise 'Unable to create builtin group.' if instance.new_record?
48 raise 'Unable to create builtin group.' if instance.new_record?
49 instance
49 instance
50 end
50 end
51 private :create_instance
51 private :create_instance
52 end
52 end
53 end
53 end
54
54
55 require_dependency "group_anonymous"
55 require_dependency "group_anonymous"
56 require_dependency "group_non_member"
56 require_dependency "group_non_member"
@@ -1,291 +1,291
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Role < ActiveRecord::Base
18 class Role < ActiveRecord::Base
19 # Custom coder for the permissions attribute that should be an
19 # Custom coder for the permissions attribute that should be an
20 # array of symbols. Rails 3 uses Psych which can be *unbelievably*
20 # array of symbols. Rails 3 uses Psych which can be *unbelievably*
21 # slow on some platforms (eg. mingw32).
21 # slow on some platforms (eg. mingw32).
22 class PermissionsAttributeCoder
22 class PermissionsAttributeCoder
23 def self.load(str)
23 def self.load(str)
24 str.to_s.scan(/:([a-z0-9_]+)/).flatten.map(&:to_sym)
24 str.to_s.scan(/:([a-z0-9_]+)/).flatten.map(&:to_sym)
25 end
25 end
26
26
27 def self.dump(value)
27 def self.dump(value)
28 YAML.dump(value)
28 YAML.dump(value)
29 end
29 end
30 end
30 end
31
31
32 # Built-in roles
32 # Built-in roles
33 BUILTIN_NON_MEMBER = 1
33 BUILTIN_NON_MEMBER = 1
34 BUILTIN_ANONYMOUS = 2
34 BUILTIN_ANONYMOUS = 2
35
35
36 ISSUES_VISIBILITY_OPTIONS = [
36 ISSUES_VISIBILITY_OPTIONS = [
37 ['all', :label_issues_visibility_all],
37 ['all', :label_issues_visibility_all],
38 ['default', :label_issues_visibility_public],
38 ['default', :label_issues_visibility_public],
39 ['own', :label_issues_visibility_own]
39 ['own', :label_issues_visibility_own]
40 ]
40 ]
41
41
42 TIME_ENTRIES_VISIBILITY_OPTIONS = [
42 TIME_ENTRIES_VISIBILITY_OPTIONS = [
43 ['all', :label_time_entries_visibility_all],
43 ['all', :label_time_entries_visibility_all],
44 ['own', :label_time_entries_visibility_own]
44 ['own', :label_time_entries_visibility_own]
45 ]
45 ]
46
46
47 USERS_VISIBILITY_OPTIONS = [
47 USERS_VISIBILITY_OPTIONS = [
48 ['all', :label_users_visibility_all],
48 ['all', :label_users_visibility_all],
49 ['members_of_visible_projects', :label_users_visibility_members_of_visible_projects]
49 ['members_of_visible_projects', :label_users_visibility_members_of_visible_projects]
50 ]
50 ]
51
51
52 scope :sorted, lambda { order(:builtin, :position) }
52 scope :sorted, lambda { order(:builtin, :position) }
53 scope :givable, lambda { order(:position).where(:builtin => 0) }
53 scope :givable, lambda { order(:position).where(:builtin => 0) }
54 scope :builtin, lambda { |*args|
54 scope :builtin, lambda { |*args|
55 compare = (args.first == true ? 'not' : '')
55 compare = (args.first == true ? 'not' : '')
56 where("#{compare} builtin = 0")
56 where("#{compare} builtin = 0")
57 }
57 }
58
58
59 before_destroy :check_deletable
59 before_destroy :check_deletable
60 has_many :workflow_rules, :dependent => :delete_all do
60 has_many :workflow_rules, :dependent => :delete_all do
61 def copy(source_role)
61 def copy(source_role)
62 WorkflowRule.copy(nil, source_role, nil, proxy_association.owner)
62 WorkflowRule.copy(nil, source_role, nil, proxy_association.owner)
63 end
63 end
64 end
64 end
65 has_and_belongs_to_many :custom_fields, :join_table => "#{table_name_prefix}custom_fields_roles#{table_name_suffix}", :foreign_key => "role_id"
65 has_and_belongs_to_many :custom_fields, :join_table => "#{table_name_prefix}custom_fields_roles#{table_name_suffix}", :foreign_key => "role_id"
66
66
67 has_and_belongs_to_many :managed_roles, :class_name => 'Role',
67 has_and_belongs_to_many :managed_roles, :class_name => 'Role',
68 :join_table => "#{table_name_prefix}roles_managed_roles#{table_name_suffix}",
68 :join_table => "#{table_name_prefix}roles_managed_roles#{table_name_suffix}",
69 :association_foreign_key => "managed_role_id"
69 :association_foreign_key => "managed_role_id"
70
70
71 has_many :member_roles, :dependent => :destroy
71 has_many :member_roles, :dependent => :destroy
72 has_many :members, :through => :member_roles
72 has_many :members, :through => :member_roles
73 acts_as_positioned :scope => :builtin
73 acts_as_positioned :scope => :builtin
74
74
75 serialize :permissions, ::Role::PermissionsAttributeCoder
75 serialize :permissions, ::Role::PermissionsAttributeCoder
76 store :settings, :accessors => [:permissions_all_trackers, :permissions_tracker_ids]
76 store :settings, :accessors => [:permissions_all_trackers, :permissions_tracker_ids]
77 attr_protected :builtin
77 attr_protected :builtin
78
78
79 validates_presence_of :name
79 validates_presence_of :name
80 validates_uniqueness_of :name
80 validates_uniqueness_of :name
81 validates_length_of :name, :maximum => 30
81 validates_length_of :name, :maximum => 30
82 validates_inclusion_of :issues_visibility,
82 validates_inclusion_of :issues_visibility,
83 :in => ISSUES_VISIBILITY_OPTIONS.collect(&:first),
83 :in => ISSUES_VISIBILITY_OPTIONS.collect(&:first),
84 :if => lambda {|role| role.respond_to?(:issues_visibility) && role.issues_visibility_changed?}
84 :if => lambda {|role| role.respond_to?(:issues_visibility) && role.issues_visibility_changed?}
85 validates_inclusion_of :users_visibility,
85 validates_inclusion_of :users_visibility,
86 :in => USERS_VISIBILITY_OPTIONS.collect(&:first),
86 :in => USERS_VISIBILITY_OPTIONS.collect(&:first),
87 :if => lambda {|role| role.respond_to?(:users_visibility) && role.users_visibility_changed?}
87 :if => lambda {|role| role.respond_to?(:users_visibility) && role.users_visibility_changed?}
88 validates_inclusion_of :time_entries_visibility,
88 validates_inclusion_of :time_entries_visibility,
89 :in => TIME_ENTRIES_VISIBILITY_OPTIONS.collect(&:first),
89 :in => TIME_ENTRIES_VISIBILITY_OPTIONS.collect(&:first),
90 :if => lambda {|role| role.respond_to?(:time_entries_visibility) && role.time_entries_visibility_changed?}
90 :if => lambda {|role| role.respond_to?(:time_entries_visibility) && role.time_entries_visibility_changed?}
91
91
92 # Copies attributes from another role, arg can be an id or a Role
92 # Copies attributes from another role, arg can be an id or a Role
93 def copy_from(arg, options={})
93 def copy_from(arg, options={})
94 return unless arg.present?
94 return unless arg.present?
95 role = arg.is_a?(Role) ? arg : Role.find_by_id(arg.to_s)
95 role = arg.is_a?(Role) ? arg : Role.find_by_id(arg.to_s)
96 self.attributes = role.attributes.dup.except("id", "name", "position", "builtin", "permissions")
96 self.attributes = role.attributes.dup.except("id", "name", "position", "builtin", "permissions")
97 self.permissions = role.permissions.dup
97 self.permissions = role.permissions.dup
98 self
98 self
99 end
99 end
100
100
101 def permissions=(perms)
101 def permissions=(perms)
102 perms = perms.collect {|p| p.to_sym unless p.blank? }.compact.uniq if perms
102 perms = perms.collect {|p| p.to_sym unless p.blank? }.compact.uniq if perms
103 write_attribute(:permissions, perms)
103 write_attribute(:permissions, perms)
104 end
104 end
105
105
106 def add_permission!(*perms)
106 def add_permission!(*perms)
107 self.permissions = [] unless permissions.is_a?(Array)
107 self.permissions = [] unless permissions.is_a?(Array)
108
108
109 permissions_will_change!
109 permissions_will_change!
110 perms.each do |p|
110 perms.each do |p|
111 p = p.to_sym
111 p = p.to_sym
112 permissions << p unless permissions.include?(p)
112 permissions << p unless permissions.include?(p)
113 end
113 end
114 save!
114 save!
115 end
115 end
116
116
117 def remove_permission!(*perms)
117 def remove_permission!(*perms)
118 return unless permissions.is_a?(Array)
118 return unless permissions.is_a?(Array)
119 permissions_will_change!
119 permissions_will_change!
120 perms.each { |p| permissions.delete(p.to_sym) }
120 perms.each { |p| permissions.delete(p.to_sym) }
121 save!
121 save!
122 end
122 end
123
123
124 # Returns true if the role has the given permission
124 # Returns true if the role has the given permission
125 def has_permission?(perm)
125 def has_permission?(perm)
126 !permissions.nil? && permissions.include?(perm.to_sym)
126 !permissions.nil? && permissions.include?(perm.to_sym)
127 end
127 end
128
128
129 def consider_workflow?
129 def consider_workflow?
130 has_permission?(:add_issues) || has_permission?(:edit_issues)
130 has_permission?(:add_issues) || has_permission?(:edit_issues)
131 end
131 end
132
132
133 def <=>(role)
133 def <=>(role)
134 if role
134 if role
135 if builtin == role.builtin
135 if builtin == role.builtin
136 position <=> role.position
136 position <=> role.position
137 else
137 else
138 builtin <=> role.builtin
138 builtin <=> role.builtin
139 end
139 end
140 else
140 else
141 -1
141 -1
142 end
142 end
143 end
143 end
144
144
145 def to_s
145 def to_s
146 name
146 name
147 end
147 end
148
148
149 def name
149 def name
150 case builtin
150 case builtin
151 when 1; l(:label_role_non_member, :default => read_attribute(:name))
151 when 1; l(:label_role_non_member, :default => read_attribute(:name))
152 when 2; l(:label_role_anonymous, :default => read_attribute(:name))
152 when 2; l(:label_role_anonymous, :default => read_attribute(:name))
153 else; read_attribute(:name)
153 else; read_attribute(:name)
154 end
154 end
155 end
155 end
156
156
157 # Return true if the role is a builtin role
157 # Return true if the role is a builtin role
158 def builtin?
158 def builtin?
159 self.builtin != 0
159 self.builtin != 0
160 end
160 end
161
161
162 # Return true if the role is the anonymous role
162 # Return true if the role is the anonymous role
163 def anonymous?
163 def anonymous?
164 builtin == 2
164 builtin == 2
165 end
165 end
166
166
167 # Return true if the role is a project member role
167 # Return true if the role is a project member role
168 def member?
168 def member?
169 !self.builtin?
169 !self.builtin?
170 end
170 end
171
171
172 # Return true if role is allowed to do the specified action
172 # Return true if role is allowed to do the specified action
173 # action can be:
173 # action can be:
174 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
174 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
175 # * a permission Symbol (eg. :edit_project)
175 # * a permission Symbol (eg. :edit_project)
176 def allowed_to?(action)
176 def allowed_to?(action)
177 if action.is_a? Hash
177 if action.is_a? Hash
178 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
178 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
179 else
179 else
180 allowed_permissions.include? action
180 allowed_permissions.include? action
181 end
181 end
182 end
182 end
183
183
184 # Return all the permissions that can be given to the role
184 # Return all the permissions that can be given to the role
185 def setable_permissions
185 def setable_permissions
186 setable_permissions = Redmine::AccessControl.permissions - Redmine::AccessControl.public_permissions
186 setable_permissions = Redmine::AccessControl.permissions - Redmine::AccessControl.public_permissions
187 setable_permissions -= Redmine::AccessControl.members_only_permissions if self.builtin == BUILTIN_NON_MEMBER
187 setable_permissions -= Redmine::AccessControl.members_only_permissions if self.builtin == BUILTIN_NON_MEMBER
188 setable_permissions -= Redmine::AccessControl.loggedin_only_permissions if self.builtin == BUILTIN_ANONYMOUS
188 setable_permissions -= Redmine::AccessControl.loggedin_only_permissions if self.builtin == BUILTIN_ANONYMOUS
189 setable_permissions
189 setable_permissions
190 end
190 end
191
191
192 def permissions_tracker_ids(*args)
192 def permissions_tracker_ids(*args)
193 if args.any?
193 if args.any?
194 Array(permissions_tracker_ids[args.first.to_s]).map(&:to_i)
194 Array(permissions_tracker_ids[args.first.to_s]).map(&:to_i)
195 else
195 else
196 super || {}
196 super || {}
197 end
197 end
198 end
198 end
199
199
200 def permissions_tracker_ids=(arg)
200 def permissions_tracker_ids=(arg)
201 h = arg.to_hash
201 h = arg.to_hash
202 h.values.each {|v| v.reject!(&:blank?)}
202 h.values.each {|v| v.reject!(&:blank?)}
203 super(h)
203 super(h)
204 end
204 end
205
205
206 # Returns true if tracker_id belongs to the list of
206 # Returns true if tracker_id belongs to the list of
207 # trackers for which permission is given
207 # trackers for which permission is given
208 def permissions_tracker_ids?(permission, tracker_id)
208 def permissions_tracker_ids?(permission, tracker_id)
209 permissions_tracker_ids(permission).include?(tracker_id)
209 permissions_tracker_ids(permission).include?(tracker_id)
210 end
210 end
211
211
212 def permissions_all_trackers
212 def permissions_all_trackers
213 super || {}
213 super || {}
214 end
214 end
215
215
216 def permissions_all_trackers=(arg)
216 def permissions_all_trackers=(arg)
217 super(arg.to_hash)
217 super(arg.to_hash)
218 end
218 end
219
219
220 # Returns true if permission is given for all trackers
220 # Returns true if permission is given for all trackers
221 def permissions_all_trackers?(permission)
221 def permissions_all_trackers?(permission)
222 permissions_all_trackers[permission.to_s].to_s != '0'
222 permissions_all_trackers[permission.to_s].to_s != '0'
223 end
223 end
224
224
225 # Returns true if permission is given for the tracker
225 # Returns true if permission is given for the tracker
226 # (explicitly or for all trackers)
226 # (explicitly or for all trackers)
227 def permissions_tracker?(permission, tracker)
227 def permissions_tracker?(permission, tracker)
228 permissions_all_trackers?(permission) ||
228 permissions_all_trackers?(permission) ||
229 permissions_tracker_ids?(permission, tracker.try(:id))
229 permissions_tracker_ids?(permission, tracker.try(:id))
230 end
230 end
231
231
232 # Sets the trackers that are allowed for a permission.
232 # Sets the trackers that are allowed for a permission.
233 # tracker_ids can be an array of tracker ids or :all for
233 # tracker_ids can be an array of tracker ids or :all for
234 # no restrictions.
234 # no restrictions.
235 #
235 #
236 # Examples:
236 # Examples:
237 # role.set_permission_trackers :add_issues, [1, 3]
237 # role.set_permission_trackers :add_issues, [1, 3]
238 # role.set_permission_trackers :add_issues, :all
238 # role.set_permission_trackers :add_issues, :all
239 def set_permission_trackers(permission, tracker_ids)
239 def set_permission_trackers(permission, tracker_ids)
240 h = {permission.to_s => (tracker_ids == :all ? '1' : '0')}
240 h = {permission.to_s => (tracker_ids == :all ? '1' : '0')}
241 self.permissions_all_trackers = permissions_all_trackers.merge(h)
241 self.permissions_all_trackers = permissions_all_trackers.merge(h)
242
242
243 h = {permission.to_s => (tracker_ids == :all ? [] : tracker_ids)}
243 h = {permission.to_s => (tracker_ids == :all ? [] : tracker_ids)}
244 self.permissions_tracker_ids = permissions_tracker_ids.merge(h)
244 self.permissions_tracker_ids = permissions_tracker_ids.merge(h)
245
245
246 self
246 self
247 end
247 end
248
248
249 # Find all the roles that can be given to a project member
249 # Find all the roles that can be given to a project member
250 def self.find_all_givable
250 def self.find_all_givable
251 Role.givable.to_a
251 Role.givable.to_a
252 end
252 end
253
253
254 # Return the builtin 'non member' role. If the role doesn't exist,
254 # Return the builtin 'non member' role. If the role doesn't exist,
255 # it will be created on the fly.
255 # it will be created on the fly.
256 def self.non_member
256 def self.non_member
257 find_or_create_system_role(BUILTIN_NON_MEMBER, 'Non member')
257 find_or_create_system_role(BUILTIN_NON_MEMBER, 'Non member')
258 end
258 end
259
259
260 # Return the builtin 'anonymous' role. If the role doesn't exist,
260 # Return the builtin 'anonymous' role. If the role doesn't exist,
261 # it will be created on the fly.
261 # it will be created on the fly.
262 def self.anonymous
262 def self.anonymous
263 find_or_create_system_role(BUILTIN_ANONYMOUS, 'Anonymous')
263 find_or_create_system_role(BUILTIN_ANONYMOUS, 'Anonymous')
264 end
264 end
265
265
266 private
266 private
267
267
268 def allowed_permissions
268 def allowed_permissions
269 @allowed_permissions ||= permissions + Redmine::AccessControl.public_permissions.collect {|p| p.name}
269 @allowed_permissions ||= permissions + Redmine::AccessControl.public_permissions.collect {|p| p.name}
270 end
270 end
271
271
272 def allowed_actions
272 def allowed_actions
273 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
273 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
274 end
274 end
275
275
276 def check_deletable
276 def check_deletable
277 raise "Cannot delete role" if members.any?
277 raise "Cannot delete role" if members.any?
278 raise "Cannot delete builtin role" if builtin?
278 raise "Cannot delete builtin role" if builtin?
279 end
279 end
280
280
281 def self.find_or_create_system_role(builtin, name)
281 def self.find_or_create_system_role(builtin, name)
282 role = where(:builtin => builtin).first
282 role = unscoped.where(:builtin => builtin).first
283 if role.nil?
283 if role.nil?
284 role = create(:name => name) do |r|
284 role = unscoped.create(:name => name) do |r|
285 r.builtin = builtin
285 r.builtin = builtin
286 end
286 end
287 raise "Unable to create the #{name} role (#{role.errors.full_messages.join(',')})." if role.new_record?
287 raise "Unable to create the #{name} role (#{role.errors.full_messages.join(',')})." if role.new_record?
288 end
288 end
289 role
289 role
290 end
290 end
291 end
291 end
@@ -1,924 +1,924
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require "digest/sha1"
18 require "digest/sha1"
19
19
20 class User < Principal
20 class User < Principal
21 include Redmine::SafeAttributes
21 include Redmine::SafeAttributes
22
22
23 # Different ways of displaying/sorting users
23 # Different ways of displaying/sorting users
24 USER_FORMATS = {
24 USER_FORMATS = {
25 :firstname_lastname => {
25 :firstname_lastname => {
26 :string => '#{firstname} #{lastname}',
26 :string => '#{firstname} #{lastname}',
27 :order => %w(firstname lastname id),
27 :order => %w(firstname lastname id),
28 :setting_order => 1
28 :setting_order => 1
29 },
29 },
30 :firstname_lastinitial => {
30 :firstname_lastinitial => {
31 :string => '#{firstname} #{lastname.to_s.chars.first}.',
31 :string => '#{firstname} #{lastname.to_s.chars.first}.',
32 :order => %w(firstname lastname id),
32 :order => %w(firstname lastname id),
33 :setting_order => 2
33 :setting_order => 2
34 },
34 },
35 :firstinitial_lastname => {
35 :firstinitial_lastname => {
36 :string => '#{firstname.to_s.gsub(/(([[:alpha:]])[[:alpha:]]*\.?)/, \'\2.\')} #{lastname}',
36 :string => '#{firstname.to_s.gsub(/(([[:alpha:]])[[:alpha:]]*\.?)/, \'\2.\')} #{lastname}',
37 :order => %w(firstname lastname id),
37 :order => %w(firstname lastname id),
38 :setting_order => 2
38 :setting_order => 2
39 },
39 },
40 :firstname => {
40 :firstname => {
41 :string => '#{firstname}',
41 :string => '#{firstname}',
42 :order => %w(firstname id),
42 :order => %w(firstname id),
43 :setting_order => 3
43 :setting_order => 3
44 },
44 },
45 :lastname_firstname => {
45 :lastname_firstname => {
46 :string => '#{lastname} #{firstname}',
46 :string => '#{lastname} #{firstname}',
47 :order => %w(lastname firstname id),
47 :order => %w(lastname firstname id),
48 :setting_order => 4
48 :setting_order => 4
49 },
49 },
50 :lastnamefirstname => {
50 :lastnamefirstname => {
51 :string => '#{lastname}#{firstname}',
51 :string => '#{lastname}#{firstname}',
52 :order => %w(lastname firstname id),
52 :order => %w(lastname firstname id),
53 :setting_order => 5
53 :setting_order => 5
54 },
54 },
55 :lastname_comma_firstname => {
55 :lastname_comma_firstname => {
56 :string => '#{lastname}, #{firstname}',
56 :string => '#{lastname}, #{firstname}',
57 :order => %w(lastname firstname id),
57 :order => %w(lastname firstname id),
58 :setting_order => 6
58 :setting_order => 6
59 },
59 },
60 :lastname => {
60 :lastname => {
61 :string => '#{lastname}',
61 :string => '#{lastname}',
62 :order => %w(lastname id),
62 :order => %w(lastname id),
63 :setting_order => 7
63 :setting_order => 7
64 },
64 },
65 :username => {
65 :username => {
66 :string => '#{login}',
66 :string => '#{login}',
67 :order => %w(login id),
67 :order => %w(login id),
68 :setting_order => 8
68 :setting_order => 8
69 },
69 },
70 }
70 }
71
71
72 MAIL_NOTIFICATION_OPTIONS = [
72 MAIL_NOTIFICATION_OPTIONS = [
73 ['all', :label_user_mail_option_all],
73 ['all', :label_user_mail_option_all],
74 ['selected', :label_user_mail_option_selected],
74 ['selected', :label_user_mail_option_selected],
75 ['only_my_events', :label_user_mail_option_only_my_events],
75 ['only_my_events', :label_user_mail_option_only_my_events],
76 ['only_assigned', :label_user_mail_option_only_assigned],
76 ['only_assigned', :label_user_mail_option_only_assigned],
77 ['only_owner', :label_user_mail_option_only_owner],
77 ['only_owner', :label_user_mail_option_only_owner],
78 ['none', :label_user_mail_option_none]
78 ['none', :label_user_mail_option_none]
79 ]
79 ]
80
80
81 has_and_belongs_to_many :groups,
81 has_and_belongs_to_many :groups,
82 :join_table => "#{table_name_prefix}groups_users#{table_name_suffix}",
82 :join_table => "#{table_name_prefix}groups_users#{table_name_suffix}",
83 :after_add => Proc.new {|user, group| group.user_added(user)},
83 :after_add => Proc.new {|user, group| group.user_added(user)},
84 :after_remove => Proc.new {|user, group| group.user_removed(user)}
84 :after_remove => Proc.new {|user, group| group.user_removed(user)}
85 has_many :changesets, :dependent => :nullify
85 has_many :changesets, :dependent => :nullify
86 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
86 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
87 has_one :rss_token, lambda {where "action='feeds'"}, :class_name => 'Token'
87 has_one :rss_token, lambda {where "action='feeds'"}, :class_name => 'Token'
88 has_one :api_token, lambda {where "action='api'"}, :class_name => 'Token'
88 has_one :api_token, lambda {where "action='api'"}, :class_name => 'Token'
89 has_one :email_address, lambda {where :is_default => true}, :autosave => true
89 has_one :email_address, lambda {where :is_default => true}, :autosave => true
90 has_many :email_addresses, :dependent => :delete_all
90 has_many :email_addresses, :dependent => :delete_all
91 belongs_to :auth_source
91 belongs_to :auth_source
92
92
93 scope :logged, lambda { where("#{User.table_name}.status <> #{STATUS_ANONYMOUS}") }
93 scope :logged, lambda { where("#{User.table_name}.status <> #{STATUS_ANONYMOUS}") }
94 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
94 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
95
95
96 acts_as_customizable
96 acts_as_customizable
97
97
98 attr_accessor :password, :password_confirmation, :generate_password
98 attr_accessor :password, :password_confirmation, :generate_password
99 attr_accessor :last_before_login_on
99 attr_accessor :last_before_login_on
100 attr_accessor :remote_ip
100 attr_accessor :remote_ip
101
101
102 # Prevents unauthorized assignments
102 # Prevents unauthorized assignments
103 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
103 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
104
104
105 LOGIN_LENGTH_LIMIT = 60
105 LOGIN_LENGTH_LIMIT = 60
106 MAIL_LENGTH_LIMIT = 60
106 MAIL_LENGTH_LIMIT = 60
107
107
108 validates_presence_of :login, :firstname, :lastname, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
108 validates_presence_of :login, :firstname, :lastname, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
109 validates_uniqueness_of :login, :if => Proc.new { |user| user.login_changed? && user.login.present? }, :case_sensitive => false
109 validates_uniqueness_of :login, :if => Proc.new { |user| user.login_changed? && user.login.present? }, :case_sensitive => false
110 # Login must contain letters, numbers, underscores only
110 # Login must contain letters, numbers, underscores only
111 validates_format_of :login, :with => /\A[a-z0-9_\-@\.]*\z/i
111 validates_format_of :login, :with => /\A[a-z0-9_\-@\.]*\z/i
112 validates_length_of :login, :maximum => LOGIN_LENGTH_LIMIT
112 validates_length_of :login, :maximum => LOGIN_LENGTH_LIMIT
113 validates_length_of :firstname, :lastname, :maximum => 30
113 validates_length_of :firstname, :lastname, :maximum => 30
114 validates_length_of :identity_url, maximum: 255
114 validates_length_of :identity_url, maximum: 255
115 validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
115 validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
116 validate :validate_password_length
116 validate :validate_password_length
117 validate do
117 validate do
118 if password_confirmation && password != password_confirmation
118 if password_confirmation && password != password_confirmation
119 errors.add(:password, :confirmation)
119 errors.add(:password, :confirmation)
120 end
120 end
121 end
121 end
122
122
123 self.valid_statuses = [STATUS_ACTIVE, STATUS_REGISTERED, STATUS_LOCKED]
123 self.valid_statuses = [STATUS_ACTIVE, STATUS_REGISTERED, STATUS_LOCKED]
124
124
125 before_validation :instantiate_email_address
125 before_validation :instantiate_email_address
126 before_create :set_mail_notification
126 before_create :set_mail_notification
127 before_save :generate_password_if_needed, :update_hashed_password
127 before_save :generate_password_if_needed, :update_hashed_password
128 before_destroy :remove_references_before_destroy
128 before_destroy :remove_references_before_destroy
129 after_save :update_notified_project_ids, :destroy_tokens, :deliver_security_notification
129 after_save :update_notified_project_ids, :destroy_tokens, :deliver_security_notification
130 after_destroy :deliver_security_notification
130 after_destroy :deliver_security_notification
131
131
132 scope :in_group, lambda {|group|
132 scope :in_group, lambda {|group|
133 group_id = group.is_a?(Group) ? group.id : group.to_i
133 group_id = group.is_a?(Group) ? group.id : group.to_i
134 where("#{User.table_name}.id IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id)
134 where("#{User.table_name}.id IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id)
135 }
135 }
136 scope :not_in_group, lambda {|group|
136 scope :not_in_group, lambda {|group|
137 group_id = group.is_a?(Group) ? group.id : group.to_i
137 group_id = group.is_a?(Group) ? group.id : group.to_i
138 where("#{User.table_name}.id NOT IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id)
138 where("#{User.table_name}.id NOT IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id)
139 }
139 }
140 scope :sorted, lambda { order(*User.fields_for_order_statement)}
140 scope :sorted, lambda { order(*User.fields_for_order_statement)}
141 scope :having_mail, lambda {|arg|
141 scope :having_mail, lambda {|arg|
142 addresses = Array.wrap(arg).map {|a| a.to_s.downcase}
142 addresses = Array.wrap(arg).map {|a| a.to_s.downcase}
143 if addresses.any?
143 if addresses.any?
144 joins(:email_addresses).where("LOWER(#{EmailAddress.table_name}.address) IN (?)", addresses).uniq
144 joins(:email_addresses).where("LOWER(#{EmailAddress.table_name}.address) IN (?)", addresses).uniq
145 else
145 else
146 none
146 none
147 end
147 end
148 }
148 }
149
149
150 def set_mail_notification
150 def set_mail_notification
151 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
151 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
152 true
152 true
153 end
153 end
154
154
155 def update_hashed_password
155 def update_hashed_password
156 # update hashed_password if password was set
156 # update hashed_password if password was set
157 if self.password && self.auth_source_id.blank?
157 if self.password && self.auth_source_id.blank?
158 salt_password(password)
158 salt_password(password)
159 end
159 end
160 end
160 end
161
161
162 alias :base_reload :reload
162 alias :base_reload :reload
163 def reload(*args)
163 def reload(*args)
164 @name = nil
164 @name = nil
165 @projects_by_role = nil
165 @projects_by_role = nil
166 @membership_by_project_id = nil
166 @membership_by_project_id = nil
167 @notified_projects_ids = nil
167 @notified_projects_ids = nil
168 @notified_projects_ids_changed = false
168 @notified_projects_ids_changed = false
169 @builtin_role = nil
169 @builtin_role = nil
170 @visible_project_ids = nil
170 @visible_project_ids = nil
171 @managed_roles = nil
171 @managed_roles = nil
172 base_reload(*args)
172 base_reload(*args)
173 end
173 end
174
174
175 def mail
175 def mail
176 email_address.try(:address)
176 email_address.try(:address)
177 end
177 end
178
178
179 def mail=(arg)
179 def mail=(arg)
180 email = email_address || build_email_address
180 email = email_address || build_email_address
181 email.address = arg
181 email.address = arg
182 end
182 end
183
183
184 def mail_changed?
184 def mail_changed?
185 email_address.try(:address_changed?)
185 email_address.try(:address_changed?)
186 end
186 end
187
187
188 def mails
188 def mails
189 email_addresses.pluck(:address)
189 email_addresses.pluck(:address)
190 end
190 end
191
191
192 def self.find_or_initialize_by_identity_url(url)
192 def self.find_or_initialize_by_identity_url(url)
193 user = where(:identity_url => url).first
193 user = where(:identity_url => url).first
194 unless user
194 unless user
195 user = User.new
195 user = User.new
196 user.identity_url = url
196 user.identity_url = url
197 end
197 end
198 user
198 user
199 end
199 end
200
200
201 def identity_url=(url)
201 def identity_url=(url)
202 if url.blank?
202 if url.blank?
203 write_attribute(:identity_url, '')
203 write_attribute(:identity_url, '')
204 else
204 else
205 begin
205 begin
206 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
206 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
207 rescue OpenIdAuthentication::InvalidOpenId
207 rescue OpenIdAuthentication::InvalidOpenId
208 # Invalid url, don't save
208 # Invalid url, don't save
209 end
209 end
210 end
210 end
211 self.read_attribute(:identity_url)
211 self.read_attribute(:identity_url)
212 end
212 end
213
213
214 # Returns the user that matches provided login and password, or nil
214 # Returns the user that matches provided login and password, or nil
215 def self.try_to_login(login, password, active_only=true)
215 def self.try_to_login(login, password, active_only=true)
216 login = login.to_s
216 login = login.to_s
217 password = password.to_s
217 password = password.to_s
218
218
219 # Make sure no one can sign in with an empty login or password
219 # Make sure no one can sign in with an empty login or password
220 return nil if login.empty? || password.empty?
220 return nil if login.empty? || password.empty?
221 user = find_by_login(login)
221 user = find_by_login(login)
222 if user
222 if user
223 # user is already in local database
223 # user is already in local database
224 return nil unless user.check_password?(password)
224 return nil unless user.check_password?(password)
225 return nil if !user.active? && active_only
225 return nil if !user.active? && active_only
226 else
226 else
227 # user is not yet registered, try to authenticate with available sources
227 # user is not yet registered, try to authenticate with available sources
228 attrs = AuthSource.authenticate(login, password)
228 attrs = AuthSource.authenticate(login, password)
229 if attrs
229 if attrs
230 user = new(attrs)
230 user = new(attrs)
231 user.login = login
231 user.login = login
232 user.language = Setting.default_language
232 user.language = Setting.default_language
233 if user.save
233 if user.save
234 user.reload
234 user.reload
235 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
235 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
236 end
236 end
237 end
237 end
238 end
238 end
239 user.update_column(:last_login_on, Time.now) if user && !user.new_record? && user.active?
239 user.update_column(:last_login_on, Time.now) if user && !user.new_record? && user.active?
240 user
240 user
241 rescue => text
241 rescue => text
242 raise text
242 raise text
243 end
243 end
244
244
245 # Returns the user who matches the given autologin +key+ or nil
245 # Returns the user who matches the given autologin +key+ or nil
246 def self.try_to_autologin(key)
246 def self.try_to_autologin(key)
247 user = Token.find_active_user('autologin', key, Setting.autologin.to_i)
247 user = Token.find_active_user('autologin', key, Setting.autologin.to_i)
248 if user
248 if user
249 user.update_column(:last_login_on, Time.now)
249 user.update_column(:last_login_on, Time.now)
250 user
250 user
251 end
251 end
252 end
252 end
253
253
254 def self.name_formatter(formatter = nil)
254 def self.name_formatter(formatter = nil)
255 USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
255 USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
256 end
256 end
257
257
258 # Returns an array of fields names than can be used to make an order statement for users
258 # Returns an array of fields names than can be used to make an order statement for users
259 # according to how user names are displayed
259 # according to how user names are displayed
260 # Examples:
260 # Examples:
261 #
261 #
262 # User.fields_for_order_statement => ['users.login', 'users.id']
262 # User.fields_for_order_statement => ['users.login', 'users.id']
263 # User.fields_for_order_statement('authors') => ['authors.login', 'authors.id']
263 # User.fields_for_order_statement('authors') => ['authors.login', 'authors.id']
264 def self.fields_for_order_statement(table=nil)
264 def self.fields_for_order_statement(table=nil)
265 table ||= table_name
265 table ||= table_name
266 name_formatter[:order].map {|field| "#{table}.#{field}"}
266 name_formatter[:order].map {|field| "#{table}.#{field}"}
267 end
267 end
268
268
269 # Return user's full name for display
269 # Return user's full name for display
270 def name(formatter = nil)
270 def name(formatter = nil)
271 f = self.class.name_formatter(formatter)
271 f = self.class.name_formatter(formatter)
272 if formatter
272 if formatter
273 eval('"' + f[:string] + '"')
273 eval('"' + f[:string] + '"')
274 else
274 else
275 @name ||= eval('"' + f[:string] + '"')
275 @name ||= eval('"' + f[:string] + '"')
276 end
276 end
277 end
277 end
278
278
279 def active?
279 def active?
280 self.status == STATUS_ACTIVE
280 self.status == STATUS_ACTIVE
281 end
281 end
282
282
283 def registered?
283 def registered?
284 self.status == STATUS_REGISTERED
284 self.status == STATUS_REGISTERED
285 end
285 end
286
286
287 def locked?
287 def locked?
288 self.status == STATUS_LOCKED
288 self.status == STATUS_LOCKED
289 end
289 end
290
290
291 def activate
291 def activate
292 self.status = STATUS_ACTIVE
292 self.status = STATUS_ACTIVE
293 end
293 end
294
294
295 def register
295 def register
296 self.status = STATUS_REGISTERED
296 self.status = STATUS_REGISTERED
297 end
297 end
298
298
299 def lock
299 def lock
300 self.status = STATUS_LOCKED
300 self.status = STATUS_LOCKED
301 end
301 end
302
302
303 def activate!
303 def activate!
304 update_attribute(:status, STATUS_ACTIVE)
304 update_attribute(:status, STATUS_ACTIVE)
305 end
305 end
306
306
307 def register!
307 def register!
308 update_attribute(:status, STATUS_REGISTERED)
308 update_attribute(:status, STATUS_REGISTERED)
309 end
309 end
310
310
311 def lock!
311 def lock!
312 update_attribute(:status, STATUS_LOCKED)
312 update_attribute(:status, STATUS_LOCKED)
313 end
313 end
314
314
315 # Returns true if +clear_password+ is the correct user's password, otherwise false
315 # Returns true if +clear_password+ is the correct user's password, otherwise false
316 def check_password?(clear_password)
316 def check_password?(clear_password)
317 if auth_source_id.present?
317 if auth_source_id.present?
318 auth_source.authenticate(self.login, clear_password)
318 auth_source.authenticate(self.login, clear_password)
319 else
319 else
320 User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
320 User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
321 end
321 end
322 end
322 end
323
323
324 # Generates a random salt and computes hashed_password for +clear_password+
324 # Generates a random salt and computes hashed_password for +clear_password+
325 # The hashed password is stored in the following form: SHA1(salt + SHA1(password))
325 # The hashed password is stored in the following form: SHA1(salt + SHA1(password))
326 def salt_password(clear_password)
326 def salt_password(clear_password)
327 self.salt = User.generate_salt
327 self.salt = User.generate_salt
328 self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
328 self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
329 self.passwd_changed_on = Time.now.change(:usec => 0)
329 self.passwd_changed_on = Time.now.change(:usec => 0)
330 end
330 end
331
331
332 # Does the backend storage allow this user to change their password?
332 # Does the backend storage allow this user to change their password?
333 def change_password_allowed?
333 def change_password_allowed?
334 return true if auth_source.nil?
334 return true if auth_source.nil?
335 return auth_source.allow_password_changes?
335 return auth_source.allow_password_changes?
336 end
336 end
337
337
338 # Returns true if the user password has expired
338 # Returns true if the user password has expired
339 def password_expired?
339 def password_expired?
340 period = Setting.password_max_age.to_i
340 period = Setting.password_max_age.to_i
341 if period.zero?
341 if period.zero?
342 false
342 false
343 else
343 else
344 changed_on = self.passwd_changed_on || Time.at(0)
344 changed_on = self.passwd_changed_on || Time.at(0)
345 changed_on < period.days.ago
345 changed_on < period.days.ago
346 end
346 end
347 end
347 end
348
348
349 def must_change_password?
349 def must_change_password?
350 (must_change_passwd? || password_expired?) && change_password_allowed?
350 (must_change_passwd? || password_expired?) && change_password_allowed?
351 end
351 end
352
352
353 def generate_password?
353 def generate_password?
354 generate_password == '1' || generate_password == true
354 generate_password == '1' || generate_password == true
355 end
355 end
356
356
357 # Generate and set a random password on given length
357 # Generate and set a random password on given length
358 def random_password(length=40)
358 def random_password(length=40)
359 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
359 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
360 chars -= %w(0 O 1 l)
360 chars -= %w(0 O 1 l)
361 password = ''
361 password = ''
362 length.times {|i| password << chars[SecureRandom.random_number(chars.size)] }
362 length.times {|i| password << chars[SecureRandom.random_number(chars.size)] }
363 self.password = password
363 self.password = password
364 self.password_confirmation = password
364 self.password_confirmation = password
365 self
365 self
366 end
366 end
367
367
368 def pref
368 def pref
369 self.preference ||= UserPreference.new(:user => self)
369 self.preference ||= UserPreference.new(:user => self)
370 end
370 end
371
371
372 def time_zone
372 def time_zone
373 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
373 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
374 end
374 end
375
375
376 def force_default_language?
376 def force_default_language?
377 Setting.force_default_language_for_loggedin?
377 Setting.force_default_language_for_loggedin?
378 end
378 end
379
379
380 def language
380 def language
381 if force_default_language?
381 if force_default_language?
382 Setting.default_language
382 Setting.default_language
383 else
383 else
384 super
384 super
385 end
385 end
386 end
386 end
387
387
388 def wants_comments_in_reverse_order?
388 def wants_comments_in_reverse_order?
389 self.pref[:comments_sorting] == 'desc'
389 self.pref[:comments_sorting] == 'desc'
390 end
390 end
391
391
392 # Return user's RSS key (a 40 chars long string), used to access feeds
392 # Return user's RSS key (a 40 chars long string), used to access feeds
393 def rss_key
393 def rss_key
394 if rss_token.nil?
394 if rss_token.nil?
395 create_rss_token(:action => 'feeds')
395 create_rss_token(:action => 'feeds')
396 end
396 end
397 rss_token.value
397 rss_token.value
398 end
398 end
399
399
400 # Return user's API key (a 40 chars long string), used to access the API
400 # Return user's API key (a 40 chars long string), used to access the API
401 def api_key
401 def api_key
402 if api_token.nil?
402 if api_token.nil?
403 create_api_token(:action => 'api')
403 create_api_token(:action => 'api')
404 end
404 end
405 api_token.value
405 api_token.value
406 end
406 end
407
407
408 # Generates a new session token and returns its value
408 # Generates a new session token and returns its value
409 def generate_session_token
409 def generate_session_token
410 token = Token.create!(:user_id => id, :action => 'session')
410 token = Token.create!(:user_id => id, :action => 'session')
411 token.value
411 token.value
412 end
412 end
413
413
414 # Returns true if token is a valid session token for the user whose id is user_id
414 # Returns true if token is a valid session token for the user whose id is user_id
415 def self.verify_session_token(user_id, token)
415 def self.verify_session_token(user_id, token)
416 return false if user_id.blank? || token.blank?
416 return false if user_id.blank? || token.blank?
417
417
418 scope = Token.where(:user_id => user_id, :value => token.to_s, :action => 'session')
418 scope = Token.where(:user_id => user_id, :value => token.to_s, :action => 'session')
419 if Setting.session_lifetime?
419 if Setting.session_lifetime?
420 scope = scope.where("created_on > ?", Setting.session_lifetime.to_i.minutes.ago)
420 scope = scope.where("created_on > ?", Setting.session_lifetime.to_i.minutes.ago)
421 end
421 end
422 if Setting.session_timeout?
422 if Setting.session_timeout?
423 scope = scope.where("updated_on > ?", Setting.session_timeout.to_i.minutes.ago)
423 scope = scope.where("updated_on > ?", Setting.session_timeout.to_i.minutes.ago)
424 end
424 end
425 scope.update_all(:updated_on => Time.now) == 1
425 scope.update_all(:updated_on => Time.now) == 1
426 end
426 end
427
427
428 # Return an array of project ids for which the user has explicitly turned mail notifications on
428 # Return an array of project ids for which the user has explicitly turned mail notifications on
429 def notified_projects_ids
429 def notified_projects_ids
430 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
430 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
431 end
431 end
432
432
433 def notified_project_ids=(ids)
433 def notified_project_ids=(ids)
434 @notified_projects_ids_changed = true
434 @notified_projects_ids_changed = true
435 @notified_projects_ids = ids.map(&:to_i).uniq.select {|n| n > 0}
435 @notified_projects_ids = ids.map(&:to_i).uniq.select {|n| n > 0}
436 end
436 end
437
437
438 # Updates per project notifications (after_save callback)
438 # Updates per project notifications (after_save callback)
439 def update_notified_project_ids
439 def update_notified_project_ids
440 if @notified_projects_ids_changed
440 if @notified_projects_ids_changed
441 ids = (mail_notification == 'selected' ? Array.wrap(notified_projects_ids).reject(&:blank?) : [])
441 ids = (mail_notification == 'selected' ? Array.wrap(notified_projects_ids).reject(&:blank?) : [])
442 members.update_all(:mail_notification => false)
442 members.update_all(:mail_notification => false)
443 members.where(:project_id => ids).update_all(:mail_notification => true) if ids.any?
443 members.where(:project_id => ids).update_all(:mail_notification => true) if ids.any?
444 end
444 end
445 end
445 end
446 private :update_notified_project_ids
446 private :update_notified_project_ids
447
447
448 def valid_notification_options
448 def valid_notification_options
449 self.class.valid_notification_options(self)
449 self.class.valid_notification_options(self)
450 end
450 end
451
451
452 # Only users that belong to more than 1 project can select projects for which they are notified
452 # Only users that belong to more than 1 project can select projects for which they are notified
453 def self.valid_notification_options(user=nil)
453 def self.valid_notification_options(user=nil)
454 # Note that @user.membership.size would fail since AR ignores
454 # Note that @user.membership.size would fail since AR ignores
455 # :include association option when doing a count
455 # :include association option when doing a count
456 if user.nil? || user.memberships.length < 1
456 if user.nil? || user.memberships.length < 1
457 MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
457 MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
458 else
458 else
459 MAIL_NOTIFICATION_OPTIONS
459 MAIL_NOTIFICATION_OPTIONS
460 end
460 end
461 end
461 end
462
462
463 # Find a user account by matching the exact login and then a case-insensitive
463 # Find a user account by matching the exact login and then a case-insensitive
464 # version. Exact matches will be given priority.
464 # version. Exact matches will be given priority.
465 def self.find_by_login(login)
465 def self.find_by_login(login)
466 login = Redmine::CodesetUtil.replace_invalid_utf8(login.to_s)
466 login = Redmine::CodesetUtil.replace_invalid_utf8(login.to_s)
467 if login.present?
467 if login.present?
468 # First look for an exact match
468 # First look for an exact match
469 user = where(:login => login).detect {|u| u.login == login}
469 user = where(:login => login).detect {|u| u.login == login}
470 unless user
470 unless user
471 # Fail over to case-insensitive if none was found
471 # Fail over to case-insensitive if none was found
472 user = where("LOWER(login) = ?", login.downcase).first
472 user = where("LOWER(login) = ?", login.downcase).first
473 end
473 end
474 user
474 user
475 end
475 end
476 end
476 end
477
477
478 def self.find_by_rss_key(key)
478 def self.find_by_rss_key(key)
479 Token.find_active_user('feeds', key)
479 Token.find_active_user('feeds', key)
480 end
480 end
481
481
482 def self.find_by_api_key(key)
482 def self.find_by_api_key(key)
483 Token.find_active_user('api', key)
483 Token.find_active_user('api', key)
484 end
484 end
485
485
486 # Makes find_by_mail case-insensitive
486 # Makes find_by_mail case-insensitive
487 def self.find_by_mail(mail)
487 def self.find_by_mail(mail)
488 having_mail(mail).first
488 having_mail(mail).first
489 end
489 end
490
490
491 # Returns true if the default admin account can no longer be used
491 # Returns true if the default admin account can no longer be used
492 def self.default_admin_account_changed?
492 def self.default_admin_account_changed?
493 !User.active.find_by_login("admin").try(:check_password?, "admin")
493 !User.active.find_by_login("admin").try(:check_password?, "admin")
494 end
494 end
495
495
496 def to_s
496 def to_s
497 name
497 name
498 end
498 end
499
499
500 CSS_CLASS_BY_STATUS = {
500 CSS_CLASS_BY_STATUS = {
501 STATUS_ANONYMOUS => 'anon',
501 STATUS_ANONYMOUS => 'anon',
502 STATUS_ACTIVE => 'active',
502 STATUS_ACTIVE => 'active',
503 STATUS_REGISTERED => 'registered',
503 STATUS_REGISTERED => 'registered',
504 STATUS_LOCKED => 'locked'
504 STATUS_LOCKED => 'locked'
505 }
505 }
506
506
507 def css_classes
507 def css_classes
508 "user #{CSS_CLASS_BY_STATUS[status]}"
508 "user #{CSS_CLASS_BY_STATUS[status]}"
509 end
509 end
510
510
511 # Returns the current day according to user's time zone
511 # Returns the current day according to user's time zone
512 def today
512 def today
513 if time_zone.nil?
513 if time_zone.nil?
514 Date.today
514 Date.today
515 else
515 else
516 time_zone.today
516 time_zone.today
517 end
517 end
518 end
518 end
519
519
520 # Returns the day of +time+ according to user's time zone
520 # Returns the day of +time+ according to user's time zone
521 def time_to_date(time)
521 def time_to_date(time)
522 if time_zone.nil?
522 if time_zone.nil?
523 time.to_date
523 time.to_date
524 else
524 else
525 time.in_time_zone(time_zone).to_date
525 time.in_time_zone(time_zone).to_date
526 end
526 end
527 end
527 end
528
528
529 def logged?
529 def logged?
530 true
530 true
531 end
531 end
532
532
533 def anonymous?
533 def anonymous?
534 !logged?
534 !logged?
535 end
535 end
536
536
537 # Returns user's membership for the given project
537 # Returns user's membership for the given project
538 # or nil if the user is not a member of project
538 # or nil if the user is not a member of project
539 def membership(project)
539 def membership(project)
540 project_id = project.is_a?(Project) ? project.id : project
540 project_id = project.is_a?(Project) ? project.id : project
541
541
542 @membership_by_project_id ||= Hash.new {|h, project_id|
542 @membership_by_project_id ||= Hash.new {|h, project_id|
543 h[project_id] = memberships.where(:project_id => project_id).first
543 h[project_id] = memberships.where(:project_id => project_id).first
544 }
544 }
545 @membership_by_project_id[project_id]
545 @membership_by_project_id[project_id]
546 end
546 end
547
547
548 # Returns the user's bult-in role
548 # Returns the user's bult-in role
549 def builtin_role
549 def builtin_role
550 @builtin_role ||= Role.non_member
550 @builtin_role ||= Role.non_member
551 end
551 end
552
552
553 # Return user's roles for project
553 # Return user's roles for project
554 def roles_for_project(project)
554 def roles_for_project(project)
555 # No role on archived projects
555 # No role on archived projects
556 return [] if project.nil? || project.archived?
556 return [] if project.nil? || project.archived?
557 if membership = membership(project)
557 if membership = membership(project)
558 membership.roles.to_a
558 membership.roles.to_a
559 elsif project.is_public?
559 elsif project.is_public?
560 project.override_roles(builtin_role)
560 project.override_roles(builtin_role)
561 else
561 else
562 []
562 []
563 end
563 end
564 end
564 end
565
565
566 # Returns a hash of user's projects grouped by roles
566 # Returns a hash of user's projects grouped by roles
567 def projects_by_role
567 def projects_by_role
568 return @projects_by_role if @projects_by_role
568 return @projects_by_role if @projects_by_role
569
569
570 hash = Hash.new([])
570 hash = Hash.new([])
571
571
572 group_class = anonymous? ? GroupAnonymous : GroupNonMember
572 group_class = anonymous? ? GroupAnonymous : GroupNonMember
573 members = Member.joins(:project, :principal).
573 members = Member.joins(:project, :principal).
574 where("#{Project.table_name}.status <> 9").
574 where("#{Project.table_name}.status <> 9").
575 where("#{Member.table_name}.user_id = ? OR (#{Project.table_name}.is_public = ? AND #{Principal.table_name}.type = ?)", self.id, true, group_class.name).
575 where("#{Member.table_name}.user_id = ? OR (#{Project.table_name}.is_public = ? AND #{Principal.table_name}.type = ?)", self.id, true, group_class.name).
576 preload(:project, :roles).
576 preload(:project, :roles).
577 to_a
577 to_a
578
578
579 members.reject! {|member| member.user_id != id && project_ids.include?(member.project_id)}
579 members.reject! {|member| member.user_id != id && project_ids.include?(member.project_id)}
580 members.each do |member|
580 members.each do |member|
581 if member.project
581 if member.project
582 member.roles.each do |role|
582 member.roles.each do |role|
583 hash[role] = [] unless hash.key?(role)
583 hash[role] = [] unless hash.key?(role)
584 hash[role] << member.project
584 hash[role] << member.project
585 end
585 end
586 end
586 end
587 end
587 end
588
588
589 hash.each do |role, projects|
589 hash.each do |role, projects|
590 projects.uniq!
590 projects.uniq!
591 end
591 end
592
592
593 @projects_by_role = hash
593 @projects_by_role = hash
594 end
594 end
595
595
596 # Returns the ids of visible projects
596 # Returns the ids of visible projects
597 def visible_project_ids
597 def visible_project_ids
598 @visible_project_ids ||= Project.visible(self).pluck(:id)
598 @visible_project_ids ||= Project.visible(self).pluck(:id)
599 end
599 end
600
600
601 # Returns the roles that the user is allowed to manage for the given project
601 # Returns the roles that the user is allowed to manage for the given project
602 def managed_roles(project)
602 def managed_roles(project)
603 if admin?
603 if admin?
604 @managed_roles ||= Role.givable.to_a
604 @managed_roles ||= Role.givable.to_a
605 else
605 else
606 membership(project).try(:managed_roles) || []
606 membership(project).try(:managed_roles) || []
607 end
607 end
608 end
608 end
609
609
610 # Returns true if user is arg or belongs to arg
610 # Returns true if user is arg or belongs to arg
611 def is_or_belongs_to?(arg)
611 def is_or_belongs_to?(arg)
612 if arg.is_a?(User)
612 if arg.is_a?(User)
613 self == arg
613 self == arg
614 elsif arg.is_a?(Group)
614 elsif arg.is_a?(Group)
615 arg.users.include?(self)
615 arg.users.include?(self)
616 else
616 else
617 false
617 false
618 end
618 end
619 end
619 end
620
620
621 # Return true if the user is allowed to do the specified action on a specific context
621 # Return true if the user is allowed to do the specified action on a specific context
622 # Action can be:
622 # Action can be:
623 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
623 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
624 # * a permission Symbol (eg. :edit_project)
624 # * a permission Symbol (eg. :edit_project)
625 # Context can be:
625 # Context can be:
626 # * a project : returns true if user is allowed to do the specified action on this project
626 # * a project : returns true if user is allowed to do the specified action on this project
627 # * an array of projects : returns true if user is allowed on every project
627 # * an array of projects : returns true if user is allowed on every project
628 # * nil with options[:global] set : check if user has at least one role allowed for this action,
628 # * nil with options[:global] set : check if user has at least one role allowed for this action,
629 # or falls back to Non Member / Anonymous permissions depending if the user is logged
629 # or falls back to Non Member / Anonymous permissions depending if the user is logged
630 def allowed_to?(action, context, options={}, &block)
630 def allowed_to?(action, context, options={}, &block)
631 if context && context.is_a?(Project)
631 if context && context.is_a?(Project)
632 return false unless context.allows_to?(action)
632 return false unless context.allows_to?(action)
633 # Admin users are authorized for anything else
633 # Admin users are authorized for anything else
634 return true if admin?
634 return true if admin?
635
635
636 roles = roles_for_project(context)
636 roles = roles_for_project(context)
637 return false unless roles
637 return false unless roles
638 roles.any? {|role|
638 roles.any? {|role|
639 (context.is_public? || role.member?) &&
639 (context.is_public? || role.member?) &&
640 role.allowed_to?(action) &&
640 role.allowed_to?(action) &&
641 (block_given? ? yield(role, self) : true)
641 (block_given? ? yield(role, self) : true)
642 }
642 }
643 elsif context && context.is_a?(Array)
643 elsif context && context.is_a?(Array)
644 if context.empty?
644 if context.empty?
645 false
645 false
646 else
646 else
647 # Authorize if user is authorized on every element of the array
647 # Authorize if user is authorized on every element of the array
648 context.map {|project| allowed_to?(action, project, options, &block)}.reduce(:&)
648 context.map {|project| allowed_to?(action, project, options, &block)}.reduce(:&)
649 end
649 end
650 elsif context
650 elsif context
651 raise ArgumentError.new("#allowed_to? context argument must be a Project, an Array of projects or nil")
651 raise ArgumentError.new("#allowed_to? context argument must be a Project, an Array of projects or nil")
652 elsif options[:global]
652 elsif options[:global]
653 # Admin users are always authorized
653 # Admin users are always authorized
654 return true if admin?
654 return true if admin?
655
655
656 # authorize if user has at least one role that has this permission
656 # authorize if user has at least one role that has this permission
657 roles = memberships.collect {|m| m.roles}.flatten.uniq
657 roles = memberships.collect {|m| m.roles}.flatten.uniq
658 roles << (self.logged? ? Role.non_member : Role.anonymous)
658 roles << (self.logged? ? Role.non_member : Role.anonymous)
659 roles.any? {|role|
659 roles.any? {|role|
660 role.allowed_to?(action) &&
660 role.allowed_to?(action) &&
661 (block_given? ? yield(role, self) : true)
661 (block_given? ? yield(role, self) : true)
662 }
662 }
663 else
663 else
664 false
664 false
665 end
665 end
666 end
666 end
667
667
668 # Is the user allowed to do the specified action on any project?
668 # Is the user allowed to do the specified action on any project?
669 # See allowed_to? for the actions and valid options.
669 # See allowed_to? for the actions and valid options.
670 #
670 #
671 # NB: this method is not used anywhere in the core codebase as of
671 # NB: this method is not used anywhere in the core codebase as of
672 # 2.5.2, but it's used by many plugins so if we ever want to remove
672 # 2.5.2, but it's used by many plugins so if we ever want to remove
673 # it it has to be carefully deprecated for a version or two.
673 # it it has to be carefully deprecated for a version or two.
674 def allowed_to_globally?(action, options={}, &block)
674 def allowed_to_globally?(action, options={}, &block)
675 allowed_to?(action, nil, options.reverse_merge(:global => true), &block)
675 allowed_to?(action, nil, options.reverse_merge(:global => true), &block)
676 end
676 end
677
677
678 def allowed_to_view_all_time_entries?(context)
678 def allowed_to_view_all_time_entries?(context)
679 allowed_to?(:view_time_entries, context) do |role, user|
679 allowed_to?(:view_time_entries, context) do |role, user|
680 role.time_entries_visibility == 'all'
680 role.time_entries_visibility == 'all'
681 end
681 end
682 end
682 end
683
683
684 # Returns true if the user is allowed to delete the user's own account
684 # Returns true if the user is allowed to delete the user's own account
685 def own_account_deletable?
685 def own_account_deletable?
686 Setting.unsubscribe? &&
686 Setting.unsubscribe? &&
687 (!admin? || User.active.where("admin = ? AND id <> ?", true, id).exists?)
687 (!admin? || User.active.where("admin = ? AND id <> ?", true, id).exists?)
688 end
688 end
689
689
690 safe_attributes 'firstname',
690 safe_attributes 'firstname',
691 'lastname',
691 'lastname',
692 'mail',
692 'mail',
693 'mail_notification',
693 'mail_notification',
694 'notified_project_ids',
694 'notified_project_ids',
695 'language',
695 'language',
696 'custom_field_values',
696 'custom_field_values',
697 'custom_fields',
697 'custom_fields',
698 'identity_url'
698 'identity_url'
699
699
700 safe_attributes 'status',
700 safe_attributes 'status',
701 'auth_source_id',
701 'auth_source_id',
702 'generate_password',
702 'generate_password',
703 'must_change_passwd',
703 'must_change_passwd',
704 :if => lambda {|user, current_user| current_user.admin?}
704 :if => lambda {|user, current_user| current_user.admin?}
705
705
706 safe_attributes 'group_ids',
706 safe_attributes 'group_ids',
707 :if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
707 :if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
708
708
709 # Utility method to help check if a user should be notified about an
709 # Utility method to help check if a user should be notified about an
710 # event.
710 # event.
711 #
711 #
712 # TODO: only supports Issue events currently
712 # TODO: only supports Issue events currently
713 def notify_about?(object)
713 def notify_about?(object)
714 if mail_notification == 'all'
714 if mail_notification == 'all'
715 true
715 true
716 elsif mail_notification.blank? || mail_notification == 'none'
716 elsif mail_notification.blank? || mail_notification == 'none'
717 false
717 false
718 else
718 else
719 case object
719 case object
720 when Issue
720 when Issue
721 case mail_notification
721 case mail_notification
722 when 'selected', 'only_my_events'
722 when 'selected', 'only_my_events'
723 # user receives notifications for created/assigned issues on unselected projects
723 # user receives notifications for created/assigned issues on unselected projects
724 object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
724 object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
725 when 'only_assigned'
725 when 'only_assigned'
726 is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
726 is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
727 when 'only_owner'
727 when 'only_owner'
728 object.author == self
728 object.author == self
729 end
729 end
730 when News
730 when News
731 # always send to project members except when mail_notification is set to 'none'
731 # always send to project members except when mail_notification is set to 'none'
732 true
732 true
733 end
733 end
734 end
734 end
735 end
735 end
736
736
737 def self.current=(user)
737 def self.current=(user)
738 RequestStore.store[:current_user] = user
738 RequestStore.store[:current_user] = user
739 end
739 end
740
740
741 def self.current
741 def self.current
742 RequestStore.store[:current_user] ||= User.anonymous
742 RequestStore.store[:current_user] ||= User.anonymous
743 end
743 end
744
744
745 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
745 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
746 # one anonymous user per database.
746 # one anonymous user per database.
747 def self.anonymous
747 def self.anonymous
748 anonymous_user = AnonymousUser.first
748 anonymous_user = AnonymousUser.unscoped.first
749 if anonymous_user.nil?
749 if anonymous_user.nil?
750 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :login => '', :status => 0)
750 anonymous_user = AnonymousUser.unscoped.create(:lastname => 'Anonymous', :firstname => '', :login => '', :status => 0)
751 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
751 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
752 end
752 end
753 anonymous_user
753 anonymous_user
754 end
754 end
755
755
756 # Salts all existing unsalted passwords
756 # Salts all existing unsalted passwords
757 # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
757 # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
758 # This method is used in the SaltPasswords migration and is to be kept as is
758 # This method is used in the SaltPasswords migration and is to be kept as is
759 def self.salt_unsalted_passwords!
759 def self.salt_unsalted_passwords!
760 transaction do
760 transaction do
761 User.where("salt IS NULL OR salt = ''").find_each do |user|
761 User.where("salt IS NULL OR salt = ''").find_each do |user|
762 next if user.hashed_password.blank?
762 next if user.hashed_password.blank?
763 salt = User.generate_salt
763 salt = User.generate_salt
764 hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
764 hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
765 User.where(:id => user.id).update_all(:salt => salt, :hashed_password => hashed_password)
765 User.where(:id => user.id).update_all(:salt => salt, :hashed_password => hashed_password)
766 end
766 end
767 end
767 end
768 end
768 end
769
769
770 protected
770 protected
771
771
772 def validate_password_length
772 def validate_password_length
773 return if password.blank? && generate_password?
773 return if password.blank? && generate_password?
774 # Password length validation based on setting
774 # Password length validation based on setting
775 if !password.nil? && password.size < Setting.password_min_length.to_i
775 if !password.nil? && password.size < Setting.password_min_length.to_i
776 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
776 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
777 end
777 end
778 end
778 end
779
779
780 def instantiate_email_address
780 def instantiate_email_address
781 email_address || build_email_address
781 email_address || build_email_address
782 end
782 end
783
783
784 private
784 private
785
785
786 def generate_password_if_needed
786 def generate_password_if_needed
787 if generate_password? && auth_source.nil?
787 if generate_password? && auth_source.nil?
788 length = [Setting.password_min_length.to_i + 2, 10].max
788 length = [Setting.password_min_length.to_i + 2, 10].max
789 random_password(length)
789 random_password(length)
790 end
790 end
791 end
791 end
792
792
793 # Delete all outstanding password reset tokens on password change.
793 # Delete all outstanding password reset tokens on password change.
794 # Delete the autologin tokens on password change to prohibit session leakage.
794 # Delete the autologin tokens on password change to prohibit session leakage.
795 # This helps to keep the account secure in case the associated email account
795 # This helps to keep the account secure in case the associated email account
796 # was compromised.
796 # was compromised.
797 def destroy_tokens
797 def destroy_tokens
798 if hashed_password_changed? || (status_changed? && !active?)
798 if hashed_password_changed? || (status_changed? && !active?)
799 tokens = ['recovery', 'autologin', 'session']
799 tokens = ['recovery', 'autologin', 'session']
800 Token.where(:user_id => id, :action => tokens).delete_all
800 Token.where(:user_id => id, :action => tokens).delete_all
801 end
801 end
802 end
802 end
803
803
804 # Removes references that are not handled by associations
804 # Removes references that are not handled by associations
805 # Things that are not deleted are reassociated with the anonymous user
805 # Things that are not deleted are reassociated with the anonymous user
806 def remove_references_before_destroy
806 def remove_references_before_destroy
807 return if self.id.nil?
807 return if self.id.nil?
808
808
809 substitute = User.anonymous
809 substitute = User.anonymous
810 Attachment.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
810 Attachment.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
811 Comment.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
811 Comment.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
812 Issue.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
812 Issue.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
813 Issue.where(['assigned_to_id = ?', id]).update_all('assigned_to_id = NULL')
813 Issue.where(['assigned_to_id = ?', id]).update_all('assigned_to_id = NULL')
814 Journal.where(['user_id = ?', id]).update_all(['user_id = ?', substitute.id])
814 Journal.where(['user_id = ?', id]).update_all(['user_id = ?', substitute.id])
815 JournalDetail.
815 JournalDetail.
816 where(["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]).
816 where(["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]).
817 update_all(['old_value = ?', substitute.id.to_s])
817 update_all(['old_value = ?', substitute.id.to_s])
818 JournalDetail.
818 JournalDetail.
819 where(["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]).
819 where(["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]).
820 update_all(['value = ?', substitute.id.to_s])
820 update_all(['value = ?', substitute.id.to_s])
821 Message.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
821 Message.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
822 News.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
822 News.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
823 # Remove private queries and keep public ones
823 # Remove private queries and keep public ones
824 ::Query.delete_all ['user_id = ? AND visibility = ?', id, ::Query::VISIBILITY_PRIVATE]
824 ::Query.delete_all ['user_id = ? AND visibility = ?', id, ::Query::VISIBILITY_PRIVATE]
825 ::Query.where(['user_id = ?', id]).update_all(['user_id = ?', substitute.id])
825 ::Query.where(['user_id = ?', id]).update_all(['user_id = ?', substitute.id])
826 TimeEntry.where(['user_id = ?', id]).update_all(['user_id = ?', substitute.id])
826 TimeEntry.where(['user_id = ?', id]).update_all(['user_id = ?', substitute.id])
827 Token.delete_all ['user_id = ?', id]
827 Token.delete_all ['user_id = ?', id]
828 Watcher.delete_all ['user_id = ?', id]
828 Watcher.delete_all ['user_id = ?', id]
829 WikiContent.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
829 WikiContent.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
830 WikiContent::Version.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
830 WikiContent::Version.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
831 end
831 end
832
832
833 # Return password digest
833 # Return password digest
834 def self.hash_password(clear_password)
834 def self.hash_password(clear_password)
835 Digest::SHA1.hexdigest(clear_password || "")
835 Digest::SHA1.hexdigest(clear_password || "")
836 end
836 end
837
837
838 # Returns a 128bits random salt as a hex string (32 chars long)
838 # Returns a 128bits random salt as a hex string (32 chars long)
839 def self.generate_salt
839 def self.generate_salt
840 Redmine::Utils.random_hex(16)
840 Redmine::Utils.random_hex(16)
841 end
841 end
842
842
843 # Send a security notification to all admins if the user has gained/lost admin privileges
843 # Send a security notification to all admins if the user has gained/lost admin privileges
844 def deliver_security_notification
844 def deliver_security_notification
845 options = {
845 options = {
846 field: :field_admin,
846 field: :field_admin,
847 value: login,
847 value: login,
848 title: :label_user_plural,
848 title: :label_user_plural,
849 url: {controller: 'users', action: 'index'}
849 url: {controller: 'users', action: 'index'}
850 }
850 }
851
851
852 deliver = false
852 deliver = false
853 if (admin? && id_changed? && active?) || # newly created admin
853 if (admin? && id_changed? && active?) || # newly created admin
854 (admin? && admin_changed? && active?) || # regular user became admin
854 (admin? && admin_changed? && active?) || # regular user became admin
855 (admin? && status_changed? && active?) # locked admin became active again
855 (admin? && status_changed? && active?) # locked admin became active again
856
856
857 deliver = true
857 deliver = true
858 options[:message] = :mail_body_security_notification_add
858 options[:message] = :mail_body_security_notification_add
859
859
860 elsif (admin? && destroyed? && active?) || # active admin user was deleted
860 elsif (admin? && destroyed? && active?) || # active admin user was deleted
861 (!admin? && admin_changed? && active?) || # admin is no longer admin
861 (!admin? && admin_changed? && active?) || # admin is no longer admin
862 (admin? && status_changed? && !active?) # admin was locked
862 (admin? && status_changed? && !active?) # admin was locked
863
863
864 deliver = true
864 deliver = true
865 options[:message] = :mail_body_security_notification_remove
865 options[:message] = :mail_body_security_notification_remove
866 end
866 end
867
867
868 if deliver
868 if deliver
869 users = User.active.where(admin: true).to_a
869 users = User.active.where(admin: true).to_a
870 Mailer.security_notification(users, options).deliver
870 Mailer.security_notification(users, options).deliver
871 end
871 end
872 end
872 end
873 end
873 end
874
874
875 class AnonymousUser < User
875 class AnonymousUser < User
876 validate :validate_anonymous_uniqueness, :on => :create
876 validate :validate_anonymous_uniqueness, :on => :create
877
877
878 self.valid_statuses = [STATUS_ANONYMOUS]
878 self.valid_statuses = [STATUS_ANONYMOUS]
879
879
880 def validate_anonymous_uniqueness
880 def validate_anonymous_uniqueness
881 # There should be only one AnonymousUser in the database
881 # There should be only one AnonymousUser in the database
882 errors.add :base, 'An anonymous user already exists.' if AnonymousUser.exists?
882 errors.add :base, 'An anonymous user already exists.' if AnonymousUser.exists?
883 end
883 end
884
884
885 def available_custom_fields
885 def available_custom_fields
886 []
886 []
887 end
887 end
888
888
889 # Overrides a few properties
889 # Overrides a few properties
890 def logged?; false end
890 def logged?; false end
891 def admin; false end
891 def admin; false end
892 def name(*args); I18n.t(:label_user_anonymous) end
892 def name(*args); I18n.t(:label_user_anonymous) end
893 def mail=(*args); nil end
893 def mail=(*args); nil end
894 def mail; nil end
894 def mail; nil end
895 def time_zone; nil end
895 def time_zone; nil end
896 def rss_key; nil end
896 def rss_key; nil end
897
897
898 def pref
898 def pref
899 UserPreference.new(:user => self)
899 UserPreference.new(:user => self)
900 end
900 end
901
901
902 # Returns the user's bult-in role
902 # Returns the user's bult-in role
903 def builtin_role
903 def builtin_role
904 @builtin_role ||= Role.anonymous
904 @builtin_role ||= Role.anonymous
905 end
905 end
906
906
907 def membership(*args)
907 def membership(*args)
908 nil
908 nil
909 end
909 end
910
910
911 def member_of?(*args)
911 def member_of?(*args)
912 false
912 false
913 end
913 end
914
914
915 # Anonymous user can not be destroyed
915 # Anonymous user can not be destroyed
916 def destroy
916 def destroy
917 false
917 false
918 end
918 end
919
919
920 protected
920 protected
921
921
922 def instantiate_email_address
922 def instantiate_email_address
923 end
923 end
924 end
924 end
General Comments 0
You need to be logged in to leave comments. Login now