Feature #4221 » enforce-password-char-types-v2.patch
app/models/setting.rb | ||
---|---|---|
19 | 19 | |
20 | 20 |
class Setting < ActiveRecord::Base |
21 | 21 | |
22 |
PASSWORD_CHAR_CLASSES = { |
|
23 |
'uppercase' => /[A-Z]/, |
|
24 |
'lowercase' => /[a-z]/, |
|
25 |
'digits' => /[0-9]/, |
|
26 |
'special_chars' => /[[:ascii:]&&[:graph:]&&[:^alnum:]]/ |
|
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_CHAR_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_char_classes.include?(k) } |
|
117 |
end |
|
115 | 118 |
validate :validate_password_length |
116 | 119 |
validate do |
117 | 120 |
if password_confirmation && password != password_confirmation |
... | ... | |
366 | 369 | |
367 | 370 |
# Generate and set a random password on given length |
368 | 371 |
def random_password(length=40) |
369 |
chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a |
|
370 |
chars -= %w(0 O 1 l) |
|
372 |
chars_list = [ |
|
373 |
('A'..'Z').to_a, |
|
374 |
('a'..'z').to_a, |
|
375 |
('0'..'9').to_a, |
|
376 |
] |
|
377 |
if Setting.password_required_char_classes.include?('special_chars') |
|
378 |
chars_list << ("\x20".."\x7e").to_a.select {|c| c =~ Setting::PASSWORD_CHAR_CLASSES['special_chars']} |
|
379 |
end |
|
380 |
chars_list.each {|v| v.reject! {|c| %(0O1l|'"`*).include?(c)}} |
|
381 | ||
371 | 382 |
password = +'' |
372 |
length.times {|i| password << chars[SecureRandom.random_number(chars.size)] } |
|
383 |
chars_list.each do |chars| |
|
384 |
password << chars[SecureRandom.random_number(chars.size)] |
|
385 |
length -= 1 |
|
386 |
end |
|
387 |
chars = chars_list.flatten |
|
388 |
length.times { password << chars[SecureRandom.random_number(chars.size)] } |
|
389 |
password = password.split('').shuffle(random: SecureRandom).join |
|
373 | 390 |
self.password = password |
374 | 391 |
self.password_confirmation = password |
375 | 392 |
self |
app/views/account/password_recovery.html.erb | ||
---|---|---|
9 | 9 |
<label for="new_password"><%=l(:field_new_password)%> <span class="required">*</span></label> |
10 | 10 |
<%= password_field_tag 'new_password', nil, :size => 25 %> |
11 | 11 |
<em class="info"><%= l(:text_caracters_minimum, :count => Setting.password_min_length) %></em> |
12 |
<% if Setting.password_required_char_classes.any? %> |
|
13 |
<em class="info"><%= l(:text_characters_must_include, :character_classes => Setting.password_required_char_classes.collect{|c| l("label_password_char_class_#{c}")}.join(", ")) %></em> |
|
14 |
<% end %> |
|
12 | 15 |
</p> |
13 | 16 | |
14 | 17 |
<p> |
app/views/account/register.html.erb | ||
---|---|---|
8 | 8 |
<p><%= f.text_field :login, :size => 25, :required => true %></p> |
9 | 9 | |
10 | 10 |
<p><%= f.password_field :password, :size => 25, :required => true %> |
11 |
<em class="info"><%= l(:text_caracters_minimum, :count => Setting.password_min_length) %></em></p> |
|
12 | ||
11 |
<em class="info"><%= l(:text_caracters_minimum, :count => Setting.password_min_length) %></em> |
|
12 |
<% if Setting.password_required_char_classes.any? %> |
|
13 |
<em class="info"><%= l(:text_characters_must_include, :character_classes => Setting.password_required_char_classes.collect{|c| l("label_password_char_class_#{c}")}.join(", ")) %></em> |
|
14 |
<% end %> |
|
15 |
</p> |
|
13 | 16 |
<p><%= f.password_field :password_confirmation, :size => 25, :required => true %></p> |
14 | 17 |
<% end %> |
15 | 18 |
app/views/my/password.html.erb | ||
---|---|---|
9 | 9 | |
10 | 10 |
<p><label for="new_password"><%=l(:field_new_password)%> <span class="required">*</span></label> |
11 | 11 |
<%= password_field_tag 'new_password', nil, :size => 25 %> |
12 |
<em class="info"><%= l(:text_caracters_minimum, :count => Setting.password_min_length) %></em></p> |
|
12 |
<em class="info"><%= l(:text_caracters_minimum, :count => Setting.password_min_length) %></em> |
|
13 |
<% if Setting.password_required_char_classes.any? %> |
|
14 |
<em class="info"><%= l(:text_characters_must_include, :character_classes => Setting.password_required_char_classes.collect{|c| l("label_password_char_class_#{c}")}.join(", ")) %></em> |
|
15 |
<% end %> |
|
16 |
</p> |
|
13 | 17 | |
14 | 18 |
<p><label for="new_password_confirmation"><%=l(:field_password_confirmation)%> <span class="required">*</span></label> |
15 | 19 |
<%= password_field_tag 'new_password_confirmation', nil, :size => 25 %></p> |
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_char_classes, Setting::PASSWORD_CHAR_CLASSES.keys.collect {|c| [l("label_password_char_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_char_classes.any? %> |
|
38 |
<em class="info"><%= l(:text_characters_must_include, :character_classes => Setting.password_required_char_classes.collect{|c| l("label_password_char_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 (A-Z)" |
|
136 |
must_include_lowercase: "must include lowercase (a-z)" |
|
137 |
must_include_digits: "must include digits (0-9)" |
|
138 |
must_include_special_chars: "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_char_classes : Required character classes for passwords |
|
440 | 445 |
setting_lost_password: Allow password reset via email |
441 | 446 |
setting_new_project_user_role_id: Role given to a non-admin user who creates a project |
442 | 447 |
setting_default_projects_modules: Default enabled modules for new projects |
... | ... | |
1061 | 1066 |
label_issue_history_properties: Property changes |
1062 | 1067 |
label_issue_history_notes: Notes |
1063 | 1068 |
label_last_tab_visited: Last visited tab |
1069 |
label_password_char_class_uppercase: Uppercase |
|
1070 |
label_password_char_class_lowercase: Lowercase |
|
1071 |
label_password_char_class_digits: Digits |
|
1072 |
label_password_char_class_special_chars: Special characters |
|
1064 | 1073 | |
1065 | 1074 |
button_login: Login |
1066 | 1075 |
button_submit: Submit |
... | ... | |
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_characters_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_char_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_CHAR_CLASSES.each do |key, regexp| |
|
544 |
with_settings :password_required_char_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_char_classes => Setting::PASSWORD_CHAR_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? |