From 51f7060aa8464f51f78403f87b3556a7ffaa1995 2015-05-10 10:26:55 From: Jean-Philippe Lang Date: 2015-05-10 10:26:55 Subject: [PATCH] Add the ability to expire passwords after a configurable number of days (#19458). Patch by Holger Just and Go MAEDA. git-svn-id: http://svn.redmine.org/redmine/trunk@14264 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index b6e2eb1..e1bc6a9 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -204,6 +204,7 @@ class ApplicationController < ActionController::Base def check_password_change if session[:pwd] if User.current.must_change_password? + flash[:error] = l(:error_password_expired) redirect_to my_password_path else session.delete(:pwd) diff --git a/app/models/user.rb b/app/models/user.rb index 8811a65..5978f06 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -323,8 +323,19 @@ class User < Principal return auth_source.allow_password_changes? end + def password_expired? + changed_on = self.passwd_changed_on || Time.at(0) + period = Setting.password_max_age.to_i + + if period.zero? + false + else + changed_on < period.days.ago + end + end + def must_change_password? - must_change_passwd? && change_password_allowed? + (must_change_passwd? || password_expired?) && change_password_allowed? end def generate_password? diff --git a/app/views/my/password.html.erb b/app/views/my/password.html.erb index c3f86b9..6ba2bfc 100644 --- a/app/views/my/password.html.erb +++ b/app/views/my/password.html.erb @@ -17,7 +17,7 @@ <%= submit_tag l(:button_apply) %> <% end %> -<% unless @user.must_change_passwd? %> +<% unless @user.must_change_passwd? || @user.password_expired? %> <% content_for :sidebar do %> <%= render :partial => 'sidebar' %> <% end %> diff --git a/app/views/settings/_authentication.html.erb b/app/views/settings/_authentication.html.erb index 77b5afc..80fb4bd 100644 --- a/app/views/settings/_authentication.html.erb +++ b/app/views/settings/_authentication.html.erb @@ -14,6 +14,10 @@

<%= setting_text_field :password_min_length, :size => 6 %>

+

+ <%= 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]} %> +

+

<%= setting_check_box :lost_password, :label => :label_password_lost %>

<%= setting_text_field :max_additional_emails, :size => 6 %>

diff --git a/config/locales/de.yml b/config/locales/de.yml index 8c069a4..c29cd60 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -1026,6 +1026,7 @@ de: setting_non_working_week_days: Arbeitsfreie Tage setting_openid: Erlaube OpenID-Anmeldung und -Registrierung setting_password_min_length: Mindestlänge des Kennworts + setting_password_max_age: Erzwinge Passwortwechsel nach setting_per_page_options: Objekte pro Seite setting_plain_text_mail: Nur reinen Text (kein HTML) senden setting_protocol: Protokoll diff --git a/config/locales/en.yml b/config/locales/en.yml index f1a04a0..1f205b0 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -204,6 +204,7 @@ en: error_attachment_too_big: "This file cannot be uploaded because it exceeds the maximum allowed file size (%{max_size})" error_session_expired: "Your session has expired. Please login again." warning_attachments_not_saved: "%{count} file(s) could not be saved." + error_password_expired: "Your password has expired or the administrator requires you to change it." mail_subject_lost_password: "Your %{value} password" mail_body_lost_password: 'To change your password, click on the following link:' @@ -386,6 +387,7 @@ en: setting_file_max_size_displayed: Maximum size of text files displayed inline setting_repository_log_display_limit: Maximum number of revisions displayed on file log setting_openid: Allow OpenID login and registration + setting_password_max_age: Require password change after setting_password_min_length: Minimum password length 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 diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 17b9a4c..54dc13a 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -224,6 +224,7 @@ fr: error_attachment_too_big: Ce fichier ne peut pas être attaché car il excède la taille maximale autorisée (%{max_size}) error_session_expired: "Votre session a expiré. Veuillez vous reconnecter." warning_attachments_not_saved: "%{count} fichier(s) n'ont pas pu être sauvegardés." + error_password_expired: "Votre mot de passe a expiré ou nécessite d'être changé." mail_subject_lost_password: "Votre mot de passe %{value}" mail_body_lost_password: 'Pour changer votre mot de passe, cliquez sur le lien suivant :' @@ -406,6 +407,7 @@ fr: setting_file_max_size_displayed: Taille maximum des fichiers texte affichés en ligne setting_repository_log_display_limit: "Nombre maximum de révisions affichées sur l'historique d'un fichier" setting_openid: "Autoriser l'authentification et l'enregistrement OpenID" + setting_password_max_age: Expiration des mots de passe après setting_password_min_length: Longueur minimum des mots de passe setting_new_project_user_role_id: Rôle donné à un utilisateur non-administrateur qui crée un projet setting_default_projects_modules: Modules activés par défaut pour les nouveaux projets diff --git a/config/settings.yml b/config/settings.yml index d2f0ff9..e5093dd 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -36,6 +36,10 @@ unsubscribe: password_min_length: format: int default: 8 +# Maximum password age in days +password_max_age: + format: int + default: 0 # Maximum number of additional email addresses per user max_additional_emails: format: int diff --git a/test/integration/account_test.rb b/test/integration/account_test.rb index 7f2f0e4..9ff6cc0 100644 --- a/test/integration/account_test.rb +++ b/test/integration/account_test.rb @@ -150,6 +150,40 @@ class AccountTest < Redmine::IntegrationTest assert_equal false, User.find_by_login('jsmith').must_change_passwd? end + def test_user_with_expired_password_should_be_forced_to_change_its_password + User.find_by_login('jsmith').update_attribute :passwd_changed_on, 14.days.ago + + with_settings :password_max_age => 7 do + post '/login', :username => 'jsmith', :password => 'jsmith' + assert_redirected_to '/my/page' + follow_redirect! + assert_redirected_to '/my/password' + + get '/issues' + assert_redirected_to '/my/password' + end + end + + def test_user_with_expired_password_should_be_able_to_change_its_password + User.find_by_login('jsmith').update_attribute :passwd_changed_on, 14.days.ago + + with_settings :password_max_age => 7 do + post '/login', :username => 'jsmith', :password => 'jsmith' + assert_redirected_to '/my/page' + follow_redirect! + assert_redirected_to '/my/password' + follow_redirect! + assert_response :success + post '/my/password', :password => 'jsmith', :new_password => 'newpassword', :new_password_confirmation => 'newpassword' + assert_redirected_to '/my/account' + follow_redirect! + assert_response :success + + assert_equal false, User.find_by_login('jsmith').must_change_passwd? + end + + end + def test_register_with_automatic_activation Setting.self_registration = '3'