Feature #4221 » enforce-password-char-types.patch
app/models/setting.rb | ||
---|---|---|
19 | 19 | |
20 | 20 |
class Setting < ActiveRecord::Base |
21 | 21 | |
22 |
PASSWORD_REQUIRED_CHARACTER_CLASSES = { |
|
23 |
'uppercase' => /[A-Z]/, |
|
24 |
'lowercase' => /[a-z]/, |
|
25 |
'digits' => /[0-9]/, |
|
26 |
'special_characters' => /[!@#$%]/ |
|
27 |
} |
|
28 | ||
22 | 29 |
DATE_FORMATS = [ |
23 | 30 |
'%Y-%m-%d', |
24 | 31 |
'%d/%m/%Y', |
app/models/user.rb | ||
---|---|---|
112 | 112 |
validates_length_of :firstname, :lastname, :maximum => 30 |
113 | 113 |
validates_length_of :identity_url, maximum: 255 |
114 | 114 |
validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true |
115 |
Setting::PASSWORD_REQUIRED_CHARACTER_CLASSES.each do |k, v| |
|
116 |
validates_format_of :password, :with => v, :message => :"must_include_#{k}", :allow_blank => true, :if => Proc.new { Setting.password_required_character_classes.include?(k) } |
|
117 |
end |
|
115 | 118 |
validate :validate_password_length |
116 | 119 |
validate do |
117 | 120 |
if password_confirmation && password != password_confirmation |
... | ... | |
367 | 370 | |
368 | 371 |
# Generate and set a random password on given length |
369 | 372 |
def random_password(length=40) |
370 |
chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
|
|
371 |
chars -= %w(0 O 1 l)
|
|
373 |
chars_list = [("a".."z").to_a, ("A".."Z").to_a, ("0".."9").to_a, '!@#$%'.split('')]
|
|
374 |
chars_list = chars_list.collect {|chars| chars -= %w(0 O 1 l) }
|
|
372 | 375 |
password = +'' |
373 |
length.times {|i| password << chars[SecureRandom.random_number(chars.size)] } |
|
376 |
chars_list.each { |chars| password << chars[SecureRandom.random_number(chars.size)]; length -= 1 } |
|
377 |
length.times { password << chars_list.flatten[SecureRandom.random_number(chars_list.flatten.size)] } |
|
378 |
password = password.split('').shuffle.join |
|
374 | 379 |
self.password = password |
375 | 380 |
self.password_confirmation = password |
376 | 381 |
self |
app/views/settings/_authentication.html.erb | ||
---|---|---|
20 | 20 | |
21 | 21 |
<p><%= setting_text_field :password_min_length, :size => 6 %></p> |
22 | 22 | |
23 |
<p><%= setting_multiselect :password_required_character_classes, Setting::PASSWORD_REQUIRED_CHARACTER_CLASSES.keys.collect {|c| [l("setting_password_required_character_class_#{c}"), c]} , :inline => true %></p> |
|
24 | ||
23 | 25 |
<p> |
24 | 26 |
<%= setting_select :password_max_age, [[l(:label_disabled), 0]] + [7, 30, 60, 90, 180, 365].collect{|days| [l('datetime.distance_in_words.x_days', :count => days), days.to_s]} %> |
25 | 27 |
</p> |
app/views/users/_form.html.erb | ||
---|---|---|
31 | 31 |
<p><%= f.select :auth_source_id, ([[l(:label_internal), ""]] + @auth_sources.collect { |a| [a.name, a.id] }), {}, :onchange => "if (this.value=='') {$('#password_fields').show();} else {$('#password_fields').hide();}" %></p> |
32 | 32 |
<% end %> |
33 | 33 |
<div id="password_fields" style="<%= 'display:none;' if @user.auth_source %>"> |
34 |
<p><%= f.password_field :password, :required => true, :size => 25 %> |
|
35 |
<em class="info"><%= l(:text_caracters_minimum, :count => Setting.password_min_length) %></em></p> |
|
34 |
<p> |
|
35 |
<%= f.password_field :password, :required => true, :size => 25 %> |
|
36 |
<em class="info"><%= l(:text_caracters_minimum, :count => Setting.password_min_length) %></em> |
|
37 |
<% if Setting.password_required_character_classes.any? %> |
|
38 |
<em class="info"><%= l(:text_caracters_must_include, :character_classes => Setting.password_required_character_classes.collect{|c| l("setting_password_required_character_class_#{c}")}.join(", ")) %></em> |
|
39 |
<% end %> |
|
40 |
</p> |
|
36 | 41 |
<p><%= f.password_field :password_confirmation, :required => true, :size => 25 %></p> |
37 | 42 |
<p><%= f.check_box :generate_password %></p> |
38 | 43 |
<p><%= f.check_box :must_change_passwd %></p> |
config/locales/en.yml | ||
---|---|---|
132 | 132 |
earlier_than_minimum_start_date: "cannot be earlier than %{date} because of preceding issues" |
133 | 133 |
not_a_regexp: "is not a valid regular expression" |
134 | 134 |
open_issue_with_closed_parent: "An open issue cannot be attached to a closed parent task" |
135 |
must_include_uppercase: "must include Uppercase" |
|
136 |
must_include_lowercase: "must include Lowercase" |
|
137 |
must_include_digits: "must include Digits" |
|
138 |
must_include_special_characters: "must include Special Characters" |
|
135 | 139 | |
136 | 140 |
actionview_instancetag_blank_option: Please select |
137 | 141 | |
... | ... | |
437 | 441 |
setting_openid: Allow OpenID login and registration |
438 | 442 |
setting_password_max_age: Require password change after |
439 | 443 |
setting_password_min_length: Minimum password length |
444 |
setting_password_required_character_classes : Required character classes for passwords |
|
445 |
setting_password_required_character_class_uppercase: Uppercase |
|
446 |
setting_password_required_character_class_lowercase: Lowercase |
|
447 |
setting_password_required_character_class_digits: Digits |
|
448 |
setting_password_required_character_class_special_characters: Special Characters |
|
440 | 449 |
setting_lost_password: Allow password reset via email |
441 | 450 |
setting_new_project_user_role_id: Role given to a non-admin user who creates a project |
442 | 451 |
setting_default_projects_modules: Default enabled modules for new projects |
... | ... | |
1152 | 1161 |
text_project_identifier_info: 'Only lower case letters (a-z), numbers, dashes and underscores are allowed.<br />Once saved, the identifier cannot be changed.' |
1153 | 1162 |
text_caracters_maximum: "%{count} characters maximum." |
1154 | 1163 |
text_caracters_minimum: "Must be at least %{count} characters long." |
1164 |
text_caracters_must_include: "Must include %{character_classes}." |
|
1155 | 1165 |
text_length_between: "Length between %{min} and %{max} characters." |
1156 | 1166 |
text_tracker_no_workflow: No workflow defined for this tracker |
1157 | 1167 |
text_role_no_workflow: No workflow defined for this role |
config/settings.yml | ||
---|---|---|
36 | 36 |
security_notifications: 1 |
37 | 37 |
unsubscribe: |
38 | 38 |
default: 1 |
39 |
password_required_character_classes: |
|
40 |
serialized: true |
|
41 |
default: [] |
|
39 | 42 |
password_min_length: |
40 | 43 |
format: int |
41 | 44 |
default: 8 |
test/unit/user_test.rb | ||
---|---|---|
539 | 539 |
end |
540 | 540 |
end |
541 | 541 | |
542 |
def test_validate_password_format |
|
543 |
Setting::PASSWORD_REQUIRED_CHARACTER_CLASSES.each do |key, regexp| |
|
544 |
with_settings :password_required_character_classes => key do |
|
545 |
user = User.new(:firstname => "new", :lastname => "user", :login => "random", :mail => "random@somnet.foo") |
|
546 |
p = 'PASSWDpasswd01234!@#$%'.gsub(regexp, '') |
|
547 |
user.password, user.password_confirmation = p, p |
|
548 |
assert !user.save |
|
549 |
assert_equal 1, user.errors.count |
|
550 |
end |
|
551 |
end |
|
552 |
end |
|
553 | ||
542 | 554 |
def test_name_format |
543 | 555 |
assert_equal 'John S.', @jsmith.name(:firstname_lastinitial) |
544 | 556 |
assert_equal 'Smith, John', @jsmith.name(:lastname_comma_firstname) |
... | ... | |
1058 | 1070 |
assert !u.password_confirmation.blank? |
1059 | 1071 |
end |
1060 | 1072 | |
1073 |
def test_random_password_include_required_characters |
|
1074 |
with_settings :password_required_character_classes => Setting::PASSWORD_REQUIRED_CHARACTER_CLASSES do |
|
1075 |
u = User.new(:firstname => "new", :lastname => "user", :login => "random", :mail => "random@somnet.foo") |
|
1076 |
u.random_password |
|
1077 |
assert u.valid? |
|
1078 |
end |
|
1079 |
end |
|
1080 | ||
1061 | 1081 |
test "#change_password_allowed? should be allowed if no auth source is set" do |
1062 | 1082 |
user = User.generate! |
1063 | 1083 |
assert user.change_password_allowed? |