Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

- Forces minimum entropy on passwords for Privileged users.

- Added a password entropy indicator for registered users.
TODO: Add translations.

Setup priviliged users
Fix functional/unit tests
Improve url_alias updating code

Front-end for #7803 - demote/promote privileged users

Resolves #7804 - refactored forgotten password reset

Update tests conform new password resets

Adds the session timeout setting and feature.

Fixes the edit profile form, it works but it is not really pretty.

- Forces minimum entropy on passwords for Privileged users.
- Added a password entropy indicator for registered users.
TODO: Add translations.

Adds password entropy settings.

Fixes the passwords_reset form for privileged users.
Put the entropy rater in a partial.
Updated the user unit test to include a strong enough password.
Added a test for the entropy feature.
  • Loading branch information...
commit ccf2ee8e4f276b5634cd8ce74298b7fadaa27a55 1 parent c0c4756
@JoostvDoorn JoostvDoorn authored Arthur Holstvoogd committed
View
44 app/models/user.rb
@@ -121,6 +121,8 @@ class User < ActiveRecord::Base
validate :should_not_allow_reserved_login
+ validate :privileged_users_password_should_be_strong
+
# Make sure the user's password (stored in the virtual attribute +password+)
# is stored as a hash after the user is created/updatedRoleAssignment.
before_save :encrypt_password
@@ -401,6 +403,48 @@ def validate_uniqueness_of_email
return !user
end
+ def privileged_users_password_should_be_strong
+ return if password.blank?
+ errors.add(:password, :password_not_strong_enough) if is_privileged? && User.password_entropy(password) < (Settler[:password_required_entropy] ? Settler[:password_required_entropy] : 66)
+ end
+
+ # Password entropy based on: http://blog.shay.co/password-entropy/
+ def self.password_entropy(password)
+ password.length == 0 ? 0 : password.length * Math.log(self.password_alphabet(password)) / Math.log(2)
+ end
+
+ # Determens the password alphabet size
+ def self.password_alphabet(password)
+ alphabet = 0
+ lower = false
+ upper = false
+ numbers = false
+ symbols1 = false
+ symbols2 = false
+ other = ""
+ password.each_char do |c|
+ if !lower && c.match('[a-z]') != nil
+ lower = true
+ alphabet += 26
+ elsif !upper && c.match('[A-Z]') != nil
+ upper = true
+ alphabet += 26
+ elsif !numbers && c.match('[0-9]') != nil
+ numbers = true
+ alphabet += 10
+ elsif !symbols1 && '!@#$%^&*()'.include?(c)
+ symbols1 = true
+ alphabet += 10
+ elsif !symbols2 && '~`-_=+[]{}\\|;:\'",.<>?/'.include?(c)
+ symbols2 = true
+ alphabet += 22
+ else
+ alphabet += 1
+ end
+ end
+ return alphabet
+ end
+
# Returns an invitation code for a user, based on the user's +email_address+.
def self.generate_invitation_code(email_address)
Digest::SHA256.hexdigest(email_address)
View
5 app/views/password_resets/edit.html.haml
@@ -1,13 +1,12 @@
%h1= t 'users.password_reset'
= error_messages_for :user
-
-- form_for @user, :url => password_reset_path(params[:id]) do |f|
+- form_for :user, @user, :url => password_reset_path(params[:id]), :html => { :method => :put, :id => "edit_user_"+@user.id.to_s } do |f|
.formFieldCt.clearfix
%label{ :for => 'user_password' }
= t 'users.password'
= f.password_field :password, :class => 'textfield'
-
+ = render :partial => 'users/password_rater'
.formFieldCt.clearfix
%label{ :for => 'user_password_confirmation' }
= t 'users.password_confirmation'
View
7 app/views/users/_password_rater.html.haml
@@ -0,0 +1,7 @@
+- if @user.is_a?(PrivilegedUser)
+- include_js 'entropy.js'
+#password_rater
+- if Settler[:password_required_entropy] && Settler[:password_good_entropy]
+ %script{:type => "text/javascript"}
+ = 'var required_entropy = ' + Settler[:password_required_entropy].to_s + ';'
+ = 'var good_entropy = ' + Settler[:password_good_entropy].to_s + ';'
View
1  app/views/users/edit.html.haml
@@ -35,6 +35,7 @@
= t 'users.password'
.left
= f.password_field :password, :class => 'textfield'
+ = render :partial => 'password_rater'
%br/
%small= t 'users.leave_blank_to_preserve_password'
View
2  config/locales/activerecord/nl.yml
@@ -343,3 +343,5 @@ nl:
login:
invalid: "kan alleen letters, cijfers en '-' bevatten."
reserved_login: "is een gereserveerde login, gelieve een andere te kiezen."
+ password:
+ password_not_strong_enough: is niet sterk genoeg. U kunt uw wachtwoord sterker maken door het langer te maken, door meer verschillende tekens te gebruiken of hoofdletters te gebruiken.
View
21 defaults/app/views/user_mailer/email_used_to_create_account.text.html.erb
@@ -0,0 +1,21 @@
+<div style="font-family: arial, sans-serif; font-size: 12px">
+ <p>
+ Beste <%=@user.login-%>,
+ </p>
+ <p>
+ U ontvangt deze e-mail omdat er een poging gedaan is om met uw e-mailadres
+ een account aan te maken bij <%= @host %>. Aangezien er voor uw e-mailadres
+ al een account bestaat bij <%= @host %>. Is dit verzoek niet uitgevoerd.
+ Als u dit zelf niet heeft gedaan kunt u dit bericht negeren.
+ </p>
+ <p>
+ Als u geen toegang heeft tot uw account, dan kunt u een nieuw wachtwoord aanvragen:
+ <%= link_to I18n.t('layouts.forgot_password'), new_password_reset_path(:host => @host) %><br/>
+ Uw gebruikersnaam is: <%=@user.login-%>
+ </p>
+ <p>
+ Met vriendelijke groet,
+ <br/><br/>
+ Het <%= @host %> team
+ </p>
+</div>
View
14 defaults/app/views/user_mailer/email_used_to_create_account.text.plain.erb
@@ -0,0 +1,14 @@
+Beste <%=@user.login-%>,
+
+U ontvangt deze e-mail omdat er een poging gedaan is om met uw e-mailadres
+een account aan te maken bij <%= @host %>. Aangezien er voor uw e-mailadres
+al een account bestaat bij <%= @host %>. Is dit verzoek niet uitgevoerd.
+Als u dit zelf niet heeft gedaan kunt u dit bericht negeren.
+
+Als u geen toegang heeft tot uw account, dan kunt u een nieuw wachtwoord aanvragen:
+<%= new_password_reset_path(:host => @host) %>
+Uw gebruikersnaam is: <%=@user.login-%>
+
+Met vriendelijke groet,
+
+Het <%= @host %> team
View
11 defaults/config/settler.yml
@@ -206,6 +206,16 @@ settings: &settings
editable: true
validations:
presence: true
+ password_required_entropy:
+ alt: De vereiste sterkte van een wachtwoord
+ value: 66
+ editable: true
+ typecast: integer
+ password_good_entropy:
+ alt: De gewenste sterkte van een wachtwoord
+ value: 156
+ editable: true
+ typecast: integer
proclaimer_page_url:
alt: URL van de pagina met de proclaimer voor de website
value: /proclaimer
@@ -218,7 +228,6 @@ settings: &settings
editable: true
validations:
presence: true
-
development:
<<: *settings
View
78 public/javascripts/entropy.js
@@ -0,0 +1,78 @@
+var entropy_state = 0;
+var privileged_user = false;
+function calculateAlphabetSize(password) {
+ var alphabet = 0, lower = false, upper = false, numbers = false, symbols1 = false, symbols2 = false, other = '', c;
+
+ for(var i = 0; i < password.length; i++) {
+ c = password[i];
+ if(!lower && 'abcdefghijklmnopqrstuvwxyz'.indexOf(c) >= 0) {
+ alphabet += 26;
+ lower = true;
+ }
+ else if(!upper && 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.indexOf(c) >= 0) {
+ alphabet += 26;
+ upper = true;
+ }
+ else if(!numbers && '0123456789'.indexOf(c) >= 0) {
+ alphabet += 10;
+ numbers = true;
+ }
+ else if(!symbols1 && '!@#$%^&*()'.indexOf(c) >= 0) {
+ alphabet += 10;
+ symbols1 = true;
+ }
+ else if(!symbols2 && '~`-_=+[]{}\\|;:\'",.<>?/'.indexOf(c) >= 0) {
+ alphabet += 22;
+ symbols2 = true;
+ }
+ else if(other.indexOf(c) === -1) {
+ alphabet += 1;
+ other += c;
+ }
+ }
+
+ return alphabet;
+}
+
+function calculateEntropy(password) {
+ if(password.length === 0) return 0;
+ var entropy = password.length * Math.log(calculateAlphabetSize(password)) / Math.log(2);
+ return (Math.round(entropy * 100) / 100);
+}
+Event.observe(window, 'load', function() {
+ privileged_user = $('password_rater') != undefined;
+ $("password_rater").hide();
+ $('password_rater').setStyle({ margin: '2px 0px', padding: '5px', width: '304px' })
+ if(privileged_user) {
+ $('user_password').observe('keyup',function(event) {
+ element = Event.element(event);
+ if(element.value != "") {
+ $("password_rater").show();
+ var entropy = calculateEntropy(element.value);
+ if(entropy > (good_entropy != undefined ? good_entropy : 156)) {
+ if(entropy_state != 1) {
+ $("password_rater").update(I18n.t('good','password_entropy'));
+ $("password_rater").setStyle({ border: '#458B00 solid 1px' })
+ entropy_state = 1;
+ }
+ }
+ else if(entropy > (required_entropy != undefined ? required_entropy : 66)) {
+ if(entropy_state != 2) {
+ $("password_rater").update(I18n.t('required','password_entropy'));
+ $("password_rater").setStyle({ border: '#458B00 dotted 1px' })
+ entropy_state = 2;
+ }
+ }
+ else if(entropy_state != 3) {
+ $("password_rater").update(I18n.t('bad','password_entropy'));
+ $("password_rater").setStyle({ border: '#FF0000 solid 1px' })
+ entropy_state = 3;
+ }
+ }
+ else if(entropy_state != 0) {
+ entropy_state = 0;
+ $("password_rater").hide();
+ }
+ });
+ }
+});
View
9 test/unit/user_test.rb
@@ -75,9 +75,14 @@ def test_should_not_update_login
assert_equal "sjoerd", users(:sjoerd).reload.login
end
- def test_should_reset_password
+ def test_should_not_reset_password_if_entropy_is_too_low
users(:gerjan).update_attributes(:password => 'new password', :password_confirmation => 'new password')
- assert_equal users(:gerjan), User.authenticate('gerjan', 'new password')
+ assert_equal nil, User.authenticate('gerjan', 'new password')
+ end
+
+ def test_should_reset_password
+ users(:gerjan).update_attributes(:password => 'new password 1234', :password_confirmation => 'new password 1234')
+ assert_equal users(:gerjan), User.authenticate('gerjan', 'new password 1234')
end
def test_should_not_rehash_password
Please sign in to comment.
Something went wrong with that request. Please try again.