Feature #3096 » login_attempts.diff
| /home/amurmann/workspace/redmine/app/controllers/account_controller.rb (working copy) | ||
|---|---|---|
| 141 | 141 |
token = Token.find_by_action_and_value('register', params[:token])
|
| 142 | 142 |
redirect_to(home_url) && return unless token and !token.expired? |
| 143 | 143 |
user = token.user |
| 144 |
redirect_to(home_url) && return unless user.status == User::STATUS_REGISTERED |
|
| 144 |
redirect_to(home_url) && return unless user.status == User::STATUS_REGISTERED || user.status == User::STATUS_LOCKED
|
|
| 145 | 145 |
user.status = User::STATUS_ACTIVE |
| 146 |
user.failed_logins = 0 |
|
| 146 | 147 |
if user.save |
| 147 | 148 |
token.destroy |
| 148 | 149 |
flash[:notice] = l(:notice_account_activated) |
| ... | ... | |
| 155 | 156 |
def password_authentication |
| 156 | 157 |
user = User.try_to_login(params[:username], params[:password]) |
| 157 | 158 |
if user.nil? |
| 158 |
# Invalid credentials |
|
| 159 |
flash.now[:error] = l(:notice_account_invalid_creditentials)
|
|
| 159 |
# Invalid credentials
|
|
| 160 |
flash.now[:error] = remaining_login_attempts_notice params[:username]
|
|
| 160 | 161 |
elsif user.new_record? |
| 161 | 162 |
# Onthefly creation failed, display the registration form to fill/fix attributes |
| 162 | 163 |
@user = user |
| ... | ... | |
| 210 | 211 |
end |
| 211 | 212 |
end |
| 212 | 213 |
|
| 213 |
def successful_authentication(user) |
|
| 214 |
def successful_authentication(user)
|
|
| 214 | 215 |
# Valid user |
| 215 | 216 |
self.logged_user = user |
| 217 |
# Reset count for failed authentication attempts |
|
| 218 |
user.failed_logins = 0 |
|
| 219 |
user.save |
|
| 216 | 220 |
# generate a key and set cookie if autologin |
| 217 | 221 |
if params[:autologin] && Setting.autologin? |
| 218 | 222 |
token = Token.create(:user => user, :action => 'autologin') |
| ... | ... | |
| 276 | 280 |
flash[:notice] = l(:notice_account_pending) |
| 277 | 281 |
redirect_to :action => 'login' |
| 278 | 282 |
end |
| 283 |
|
|
| 284 |
def remaining_login_attempts_notice(user_name) |
|
| 285 |
user = User.find_by_login(user_name) |
|
| 286 |
allowed_attempts = Setting.allowed_login_attempts.to_i |
|
| 287 |
return l(:notice_account_invalid_creditentials) if user.nil? || allowed_attempts == 0 |
|
| 288 |
if allowed_attempts - user.failed_logins > 0 |
|
| 289 |
s = l(:notice_account_invalid_creditentials) |
|
| 290 |
s << " " |
|
| 291 |
s << l(:notice_remaining_login_attempts, :value => (allowed_attempts - user.failed_logins)) |
|
| 292 |
else |
|
| 293 |
s = l(:notice_too_many_login_attempts) |
|
| 294 |
end |
|
| 295 |
s |
|
| 296 |
end |
|
| 297 |
|
|
| 279 | 298 |
end |
| /home/amurmann/workspace/redmine/app/models/mailer.rb (working copy) | ||
|---|---|---|
| 215 | 215 |
subject 'Redmine test' |
| 216 | 216 |
body :url => url_for(:controller => 'welcome') |
| 217 | 217 |
end |
| 218 | ||
| 218 |
|
|
| 219 |
def account_locked(token, user) |
|
| 220 |
set_language_if_valid(user.language) |
|
| 221 |
recipients user.mail |
|
| 222 |
subject l(:mail_subject_account_locked, :value => Setting.app_title) |
|
| 223 |
body :project => Setting.app_title, :token => token, |
|
| 224 |
:url => url_for(:controller => 'account', :action => 'activate', :token => token.value) |
|
| 225 |
end |
|
| 226 |
|
|
| 227 |
def admin_info_locked_account(user, admin_mail) |
|
| 228 |
recipients admin_mail |
|
| 229 |
subject l(:mail_subject_admin_info_locked_account, :value => user.login) |
|
| 230 |
body :value => user.login |
|
| 231 |
end |
|
| 232 |
|
|
| 219 | 233 |
# Overrides default deliver! method to prevent from sending an email |
| 220 | 234 |
# with no recipient, cc or bcc |
| 221 | 235 |
def deliver!(mail = @mail) |
| /home/amurmann/workspace/redmine/app/models/user.rb (working copy) | ||
|---|---|---|
| 50 | 50 |
attr_accessor :password, :password_confirmation |
| 51 | 51 |
attr_accessor :last_before_login_on |
| 52 | 52 |
# Prevents unauthorized assignments |
| 53 |
attr_protected :login, :admin, :password, :password_confirmation, :hashed_password |
|
| 53 |
attr_protected :login, :admin, :password, :password_confirmation, :hashed_password, :failed_logins
|
|
| 54 | 54 |
|
| 55 | 55 |
validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
|
| 56 | 56 |
validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? }
|
| ... | ... | |
| 101 | 101 |
if user |
| 102 | 102 |
# user is already in local database |
| 103 | 103 |
return nil if !user.active? |
| 104 |
if user.auth_source |
|
| 104 |
if user.auth_source
|
|
| 105 | 105 |
# user has an external authentication method |
| 106 |
return nil unless user.auth_source.authenticate(login, password) |
|
| 106 |
if !user.auth_source.authenticate(login, password) |
|
| 107 |
user.authentication_failed user |
|
| 108 |
return nil |
|
| 109 |
end |
|
| 107 | 110 |
else |
| 108 | 111 |
# authentication with local password |
| 109 |
return nil unless User.hash_password(password) == user.hashed_password |
|
| 112 |
if User.hash_password(password) != user.hashed_password |
|
| 113 |
user.authentication_failed |
|
| 114 |
return nil |
|
| 115 |
end |
|
| 110 | 116 |
end |
| 111 | 117 |
else |
| 112 | 118 |
# user is not yet registered, try to authenticate with available sources |
| ... | ... | |
| 127 | 133 |
raise text |
| 128 | 134 |
end |
| 129 | 135 |
|
| 136 |
def authentication_failed |
|
| 137 |
self.failed_logins = self.failed_logins + 1 |
|
| 138 |
if self.failed_logins >= Setting.allowed_login_attempts.to_i && Setting.allowed_login_attempts != 0 |
|
| 139 |
self.status = 3 |
|
| 140 |
token = Token.new(:user => self, :action => "register") |
|
| 141 |
token.save |
|
| 142 |
Mailer.deliver_account_locked(token, self) |
|
| 143 |
if !Setting.too_many_logins_email.empty? |
|
| 144 |
logger.info("Sending admin mail.")
|
|
| 145 |
Mailer.deliver_admin_info_locked_account(self, Setting.too_many_logins_email) |
|
| 146 |
end |
|
| 147 |
end |
|
| 148 |
self.save |
|
| 149 |
end |
|
| 150 |
|
|
| 130 | 151 |
# Returns the user who matches the given autologin +key+ or nil |
| 131 | 152 |
def self.try_to_autologin(key) |
| 132 | 153 |
token = Token.find_by_action_and_value('autologin', key)
|
| /home/amurmann/workspace/redmine/app/views/mailer/account_locked.text.html.rhtml (revision 0) | ||
|---|---|---|
| 1 |
<p><%= l(:mail_body_account_locked, :value => @project) %></p> |
|
| 2 |
<%= auto_link(@url) %></p> |
|
| /home/amurmann/workspace/redmine/app/views/mailer/account_locked.text.plain.rhtml (revision 0) | ||
|---|---|---|
| 1 |
<%= l(:mail_body_account_locked, :value => @project) %> |
|
| 2 |
<%= auto_link(@url) %> |
|
| /home/amurmann/workspace/redmine/app/views/mailer/admin_info_locked_account.html.rhtml (revision 0) | ||
|---|---|---|
| 1 |
<p><%= l(:mail_body_admin_info_locked_account, :value => @value) %></p> |
|
| /home/amurmann/workspace/redmine/app/views/mailer/admin_info_locked_account.plain.rhtml (revision 0) | ||
|---|---|---|
| 1 |
<%= l(:mail_body_admin_info_locked_account, :value => @value) %> |
|
| /home/amurmann/workspace/redmine/app/views/settings/_authentication.rhtml (working copy) | ||
|---|---|---|
| 20 | 20 | |
| 21 | 21 |
<p><label><%= l(:setting_openid) %></label> |
| 22 | 22 |
<%= check_box_tag 'settings[openid]', 1, Setting.openid?, :disabled => !Object.const_defined?(:OpenID) %><%= hidden_field_tag 'settings[openid]', 0 %></p> |
| 23 | ||
| 24 |
<p><label><%= l(:label_allowed_login_attempts) %></label> |
|
| 25 |
<%= text_field_tag 'settings[allowed_login_attempts]', Setting.allowed_login_attempts, :size => 6%><br /><em><%= l(:text_login_attempts) %></em></p> |
|
| 26 | ||
| 27 |
<p><label><%= l(:label_too_many_logins_email) %></label> |
|
| 28 |
<%= text_field_tag 'settings[too_many_logins_email]', Setting.too_many_logins_email, :size => 60%><br /><em><%= l(:text_account_locked_notification_address) %></em></p> |
|
| 23 | 29 |
</div> |
| 24 | 30 | |
| 25 | 31 |
<div style="float:right;"> |
| /home/amurmann/workspace/redmine/config/locales/en.yml (working copy) | ||
|---|---|---|
| 107 | 107 |
general_first_day_of_week: '7' |
| 108 | 108 |
|
| 109 | 109 |
notice_account_updated: Account was successfully updated. |
| 110 |
notice_account_invalid_creditentials: Invalid user or password |
|
| 110 |
notice_account_invalid_creditentials: Invalid user or password.
|
|
| 111 | 111 |
notice_account_password_updated: Password was successfully updated. |
| 112 | 112 |
notice_account_wrong_password: Wrong password |
| 113 | 113 |
notice_account_register_done: Account was successfully created. To activate your account, click on the link that was emailed to you. |
| ... | ... | |
| 130 | 130 |
notice_account_pending: "Your account was created and is now pending administrator approval." |
| 131 | 131 |
notice_default_data_loaded: Default configuration successfully loaded. |
| 132 | 132 |
notice_unable_delete_version: Unable to delete version. |
| 133 |
notice_remaining_login_attempts: "{{value}} login attempt(s) left."
|
|
| 134 |
notice_too_many_login_attempts: "Too many login attempts. Your account has been locked and an administrator has been informed." |
|
| 133 | 135 |
|
| 134 | 136 |
error_can_t_load_default_data: "Default configuration could not be loaded: {{value}}"
|
| 135 | 137 |
error_scm_not_found: "The entry or revision was not found in the repository." |
| ... | ... | |
| 149 | 151 |
mail_body_account_activation_request: "A new user ({{value}}) has registered. The account is pending your approval:"
|
| 150 | 152 |
mail_subject_reminder: "{{count}} issue(s) due in the next days"
|
| 151 | 153 |
mail_body_reminder: "{{count}} issue(s) that are assigned to you are due in the next {{days}} days:"
|
| 154 |
mail_subject_account_locked: "Your {{value}} account has been locked"
|
|
| 155 |
mail_body_account_locked: "You or someone else attempted too often to log in to your {{value}} account. Your account has been locked to protect you and your project. To reactivate your account, click on the following link: "
|
|
| 156 |
mail_body_admin_info_locked_account: "Due to too many failed login attempts, the account {{value}} has been locked."
|
|
| 157 |
mail_subject_admin_info_locked_account: "Account {{value}} locked."
|
|
| 152 | 158 |
|
| 153 | 159 |
gui_validation_error: 1 error |
| 154 | 160 |
gui_validation_error_plural: "{{count}} errors"
|
| ... | ... | |
| 668 | 674 |
label_ascending: Ascending |
| 669 | 675 |
label_descending: Descending |
| 670 | 676 |
label_date_from_to: From {{start}} to {{end}}
|
| 677 |
label_allowed_login_attempts: Allowed login attempts |
|
| 678 |
label_too_many_logins_email: Notify admin if locked |
|
| 671 | 679 |
|
| 672 | 680 |
button_login: Login |
| 673 | 681 |
button_submit: Submit |
| ... | ... | |
| 761 | 769 |
text_repository_usernames_mapping: "Select or update the Redmine user mapped to each username found in the repository log.\nUsers with the same Redmine and repository username or email are automatically mapped." |
| 762 | 770 |
text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.' |
| 763 | 771 |
text_custom_field_possible_values_info: 'One line for each value' |
| 772 |
text_login_attempts: 'Put 0 to allow unlimited login attempts.' |
|
| 773 |
text_account_locked_notification_address: "Set an administrator's email address to be notified if an account was locked because of too many login attempts.\nLeave this empty to notify noone." |
|
| 764 | 774 |
|
| 765 | 775 |
default_role_manager: Manager |
| 766 | 776 |
default_role_developper: Developer |
| /home/amurmann/workspace/redmine/config/settings.yml (working copy) | ||
|---|---|---|
| 150 | 150 |
default: 0 |
| 151 | 151 |
openid: |
| 152 | 152 |
default: 0 |
| 153 |
allowed_login_attempts: |
|
| 154 |
default: 0 |
|
| 155 |
too_many_logins_email: |
|
| 156 |
default: '' |
|
| /home/amurmann/workspace/redmine/db/migrate/20090405025700_add_failed_logins_to_users.rb (revision 0) | ||
|---|---|---|
| 1 |
class AddFailedLoginsToUsers < ActiveRecord::Migration |
|
| 2 |
def self.up |
|
| 3 |
add_column :users, :failed_logins, :integer, :default => 0 |
|
| 4 |
end |
|
| 5 | ||
| 6 |
def self.down |
|
| 7 |
remove_column :users, :failed_logins |
|
| 8 |
end |
|
| 9 |
end |
|
| /home/amurmann/workspace/redmine/test/unit/user_test.rb (working copy) | ||
|---|---|---|
| 95 | 95 |
assert_equal User.hash_password("hello"), user.hashed_password
|
| 96 | 96 |
end |
| 97 | 97 |
|
| 98 |
def test_login_attempts |
|
| 99 |
Setting.allowed_login_attempts = 2 |
|
| 100 |
user = User.try_to_login("admin", "wrong")
|
|
| 101 |
assert_equal nil, user |
|
| 102 |
user = User.try_to_login("admin", "admin")
|
|
| 103 |
assert_equal "admin", user.login |
|
| 104 |
|
|
| 105 |
user = User.try_to_login("admin", "wrong")
|
|
| 106 |
user = User.try_to_login("admin", "wrong")
|
|
| 107 |
user = User.try_to_login("admin", "admin")
|
|
| 108 |
assert_equal nil, user |
|
| 109 |
end |
|
| 110 |
|
|
| 98 | 111 |
def test_name_format |
| 99 | 112 |
assert_equal 'Smith, John', @jsmith.name(:lastname_coma_firstname) |
| 100 | 113 |
Setting.user_format = :firstname_lastname |