mirror of
https://github.com/meineerde/redmine.git
synced 2025-10-17 17:01:01 +00:00
Force passwords to contain specified character classes (#4221).
Patch by Takenori TAKAKI. git-svn-id: http://svn.redmine.org/redmine/trunk@18411 e93f8b46-1217-0410-a6f0-8f06a7374b81
This commit is contained in:
parent
0d9f7ee64a
commit
35e6a532f5
@ -19,6 +19,13 @@
|
||||
|
||||
class Setting < ActiveRecord::Base
|
||||
|
||||
PASSWORD_CHAR_CLASSES = {
|
||||
'uppercase' => /[A-Z]/,
|
||||
'lowercase' => /[a-z]/,
|
||||
'digits' => /[0-9]/,
|
||||
'special_chars' => /[[:ascii:]&&[:graph:]&&[:^alnum:]]/
|
||||
}
|
||||
|
||||
DATE_FORMATS = [
|
||||
'%Y-%m-%d',
|
||||
'%d/%m/%Y',
|
||||
|
||||
@ -112,6 +112,9 @@ class User < Principal
|
||||
validates_length_of :firstname, :lastname, :maximum => 30
|
||||
validates_length_of :identity_url, maximum: 255
|
||||
validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
|
||||
Setting::PASSWORD_CHAR_CLASSES.each do |k, v|
|
||||
validates_format_of :password, :with => v, :message => :"must_contain_#{k}", :allow_blank => true, :if => Proc.new {Setting.password_required_char_classes.include?(k)}
|
||||
end
|
||||
validate :validate_password_length
|
||||
validate do
|
||||
if password_confirmation && password != password_confirmation
|
||||
@ -366,10 +369,22 @@ class User < Principal
|
||||
|
||||
# Generate and set a random password on given length
|
||||
def random_password(length=40)
|
||||
chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
|
||||
chars -= %w(0 O 1 l)
|
||||
chars_list = [('A'..'Z').to_a, ('a'..'z').to_a, ('0'..'9').to_a]
|
||||
# auto-generated passwords contain special characters only when admins
|
||||
# require users to use passwords which contains special characters
|
||||
if Setting.password_required_char_classes.include?('special_chars')
|
||||
chars_list << ("\x20".."\x7e").to_a.select {|c| c =~ Setting::PASSWORD_CHAR_CLASSES['special_chars']}
|
||||
end
|
||||
chars_list.each {|v| v.reject! {|c| %(0O1l|'"`*).include?(c)}}
|
||||
|
||||
password = +''
|
||||
length.times {|i| password << chars[SecureRandom.random_number(chars.size)] }
|
||||
chars_list.each do |chars|
|
||||
password << chars[SecureRandom.random_number(chars.size)]
|
||||
length -= 1
|
||||
end
|
||||
chars = chars_list.flatten
|
||||
length.times { password << chars[SecureRandom.random_number(chars.size)] }
|
||||
password = password.split('').shuffle(random: SecureRandom).join
|
||||
self.password = password
|
||||
self.password_confirmation = password
|
||||
self
|
||||
|
||||
@ -9,6 +9,9 @@
|
||||
<label for="new_password"><%=l(:field_new_password)%> <span class="required">*</span></label>
|
||||
<%= password_field_tag 'new_password', nil, :size => 25 %>
|
||||
<em class="info"><%= l(:text_caracters_minimum, :count => Setting.password_min_length) %></em>
|
||||
<% if Setting.password_required_char_classes.any? %>
|
||||
<em class="info"><%= l(:text_characters_must_contain, :character_classes => Setting.password_required_char_classes.collect{|c| l("label_password_char_class_#{c}")}.join(", ")) %></em>
|
||||
<% end %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
|
||||
@ -8,8 +8,11 @@
|
||||
<p><%= f.text_field :login, :size => 25, :required => true %></p>
|
||||
|
||||
<p><%= f.password_field :password, :size => 25, :required => true %>
|
||||
<em class="info"><%= l(:text_caracters_minimum, :count => Setting.password_min_length) %></em></p>
|
||||
|
||||
<em class="info"><%= l(:text_caracters_minimum, :count => Setting.password_min_length) %></em>
|
||||
<% if Setting.password_required_char_classes.any? %>
|
||||
<em class="info"><%= l(:text_characters_must_contain, :character_classes => Setting.password_required_char_classes.collect{|c| l("label_password_char_class_#{c}")}.join(", ")) %></em>
|
||||
<% end %>
|
||||
</p>
|
||||
<p><%= f.password_field :password_confirmation, :size => 25, :required => true %></p>
|
||||
<% end %>
|
||||
|
||||
|
||||
@ -9,7 +9,11 @@
|
||||
|
||||
<p><label for="new_password"><%=l(:field_new_password)%> <span class="required">*</span></label>
|
||||
<%= password_field_tag 'new_password', nil, :size => 25 %>
|
||||
<em class="info"><%= l(:text_caracters_minimum, :count => Setting.password_min_length) %></em></p>
|
||||
<em class="info"><%= l(:text_caracters_minimum, :count => Setting.password_min_length) %></em>
|
||||
<% if Setting.password_required_char_classes.any? %>
|
||||
<em class="info"><%= l(:text_characters_must_contain, :character_classes => Setting.password_required_char_classes.collect{|c| l("label_password_char_class_#{c}")}.join(", ")) %></em>
|
||||
<% end %>
|
||||
</p>
|
||||
|
||||
<p><label for="new_password_confirmation"><%=l(:field_password_confirmation)%> <span class="required">*</span></label>
|
||||
<%= password_field_tag 'new_password_confirmation', nil, :size => 25 %></p>
|
||||
|
||||
@ -20,6 +20,8 @@
|
||||
|
||||
<p><%= setting_text_field :password_min_length, :size => 6 %></p>
|
||||
|
||||
<p><%= setting_multiselect :password_required_char_classes, Setting::PASSWORD_CHAR_CLASSES.keys.collect {|c| [l("label_password_char_class_#{c}"), c]} , :inline => true %></p>
|
||||
|
||||
<p>
|
||||
<%= 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]} %>
|
||||
</p>
|
||||
|
||||
@ -31,8 +31,13 @@
|
||||
<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>
|
||||
<% end %>
|
||||
<div id="password_fields" style="<%= 'display:none;' if @user.auth_source %>">
|
||||
<p><%= f.password_field :password, :required => true, :size => 25 %>
|
||||
<em class="info"><%= l(:text_caracters_minimum, :count => Setting.password_min_length) %></em></p>
|
||||
<p>
|
||||
<%= f.password_field :password, :required => true, :size => 25 %>
|
||||
<em class="info"><%= l(:text_caracters_minimum, :count => Setting.password_min_length) %></em>
|
||||
<% if Setting.password_required_char_classes.any? %>
|
||||
<em class="info"><%= l(:text_characters_must_contain, :character_classes => Setting.password_required_char_classes.collect{|c| l("label_password_char_class_#{c}")}.join(", ")) %></em>
|
||||
<% end %>
|
||||
</p>
|
||||
<p><%= f.password_field :password_confirmation, :required => true, :size => 25 %></p>
|
||||
<p><%= f.check_box :generate_password %></p>
|
||||
<p><%= f.check_box :must_change_passwd %></p>
|
||||
|
||||
@ -132,6 +132,10 @@ en:
|
||||
earlier_than_minimum_start_date: "cannot be earlier than %{date} because of preceding issues"
|
||||
not_a_regexp: "is not a valid regular expression"
|
||||
open_issue_with_closed_parent: "An open issue cannot be attached to a closed parent task"
|
||||
must_contain_uppercase: "must contain uppercase letters (A-Z)"
|
||||
must_contain_lowercase: "must contain lowercase letters (a-z)"
|
||||
must_contain_digits: "must contain digits (0-9)"
|
||||
must_contain_special_chars: "must contain special characters (!, $, %, ...)"
|
||||
|
||||
actionview_instancetag_blank_option: Please select
|
||||
|
||||
@ -437,6 +441,7 @@ en:
|
||||
setting_openid: Allow OpenID login and registration
|
||||
setting_password_max_age: Require password change after
|
||||
setting_password_min_length: Minimum password length
|
||||
setting_password_required_char_classes : Required character classes for passwords
|
||||
setting_lost_password: Allow password reset via email
|
||||
setting_new_project_user_role_id: Role given to a non-admin user who creates a project
|
||||
setting_default_projects_modules: Default enabled modules for new projects
|
||||
@ -1061,6 +1066,10 @@ en:
|
||||
label_issue_history_properties: Property changes
|
||||
label_issue_history_notes: Notes
|
||||
label_last_tab_visited: Last visited tab
|
||||
label_password_char_class_uppercase: uppercase letters
|
||||
label_password_char_class_lowercase: lowercase letters
|
||||
label_password_char_class_digits: digits
|
||||
label_password_char_class_special_chars: special characters
|
||||
|
||||
button_login: Login
|
||||
button_submit: Submit
|
||||
@ -1152,6 +1161,7 @@ en:
|
||||
text_project_identifier_info: 'Only lower case letters (a-z), numbers, dashes and underscores are allowed.<br />Once saved, the identifier cannot be changed.'
|
||||
text_caracters_maximum: "%{count} characters maximum."
|
||||
text_caracters_minimum: "Must be at least %{count} characters long."
|
||||
text_characters_must_contain: "Must contain %{character_classes}."
|
||||
text_length_between: "Length between %{min} and %{max} characters."
|
||||
text_tracker_no_workflow: No workflow defined for this tracker
|
||||
text_role_no_workflow: No workflow defined for this role
|
||||
|
||||
@ -36,6 +36,9 @@ lost_password:
|
||||
security_notifications: 1
|
||||
unsubscribe:
|
||||
default: 1
|
||||
password_required_char_classes:
|
||||
serialized: true
|
||||
default: []
|
||||
password_min_length:
|
||||
format: int
|
||||
default: 8
|
||||
|
||||
@ -539,6 +539,18 @@ class UserTest < ActiveSupport::TestCase
|
||||
end
|
||||
end
|
||||
|
||||
def test_validate_password_format
|
||||
Setting::PASSWORD_CHAR_CLASSES.each do |key, regexp|
|
||||
with_settings :password_required_char_classes => key do
|
||||
user = User.new(:firstname => "new", :lastname => "user", :login => "random", :mail => "random@somnet.foo")
|
||||
p = 'PASSWDpasswd01234!@#$%'.gsub(regexp, '')
|
||||
user.password, user.password_confirmation = p, p
|
||||
assert !user.save
|
||||
assert_equal 1, user.errors.count
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_name_format
|
||||
assert_equal 'John S.', @jsmith.name(:firstname_lastinitial)
|
||||
assert_equal 'Smith, John', @jsmith.name(:lastname_comma_firstname)
|
||||
@ -1058,6 +1070,14 @@ class UserTest < ActiveSupport::TestCase
|
||||
assert !u.password_confirmation.blank?
|
||||
end
|
||||
|
||||
def test_random_password_include_required_characters
|
||||
with_settings :password_required_char_classes => Setting::PASSWORD_CHAR_CLASSES do
|
||||
u = User.new(:firstname => "new", :lastname => "user", :login => "random", :mail => "random@somnet.foo")
|
||||
u.random_password
|
||||
assert u.valid?
|
||||
end
|
||||
end
|
||||
|
||||
test "#change_password_allowed? should be allowed if no auth source is set" do
|
||||
user = User.generate!
|
||||
assert user.change_password_allowed?
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user