Project

General

Profile

Feature #4221 » enforce-password-char-types-v2.patch

Takenori TAKAKI, 2019-08-27 09:37

View differences:

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?
(2-2/3)