Merge pull request #1106 from gitmarcia/admin_password_expired
Expire admin passwords once a year #885
This commit is contained in:
1
Gemfile
1
Gemfile
@@ -23,6 +23,7 @@ gem 'turbolinks'
|
||||
gem 'sprockets', '~> 3.6.3'
|
||||
|
||||
gem 'devise', '~> 3.5.7'
|
||||
gem 'devise_security_extension'
|
||||
# Use ActiveModel has_secure_password
|
||||
# gem 'bcrypt', '~> 3.1.7'
|
||||
gem 'omniauth'
|
||||
|
||||
@@ -138,6 +138,9 @@ GEM
|
||||
warden (~> 1.2.3)
|
||||
devise-async (0.10.2)
|
||||
devise (>= 3.2, < 4.0)
|
||||
devise_security_extension (0.10.0)
|
||||
devise (>= 3.0.0, < 4.0)
|
||||
railties (>= 3.2.6, < 5.0)
|
||||
diff-lcs (1.2.5)
|
||||
docile (1.1.5)
|
||||
easy_translate (0.5.0)
|
||||
@@ -460,6 +463,7 @@ DEPENDENCIES
|
||||
delayed_job_active_record (~> 4.1.0)
|
||||
devise (~> 3.5.7)
|
||||
devise-async
|
||||
devise_security_extension
|
||||
email_spec
|
||||
factory_girl_rails
|
||||
faker
|
||||
|
||||
@@ -2,8 +2,8 @@ class User < ActiveRecord::Base
|
||||
|
||||
include Verification
|
||||
|
||||
devise :database_authenticatable, :registerable, :confirmable,
|
||||
:recoverable, :rememberable, :trackable, :validatable, :omniauthable, :async
|
||||
devise :database_authenticatable, :registerable, :confirmable, :recoverable, :rememberable,
|
||||
:trackable, :validatable, :omniauthable, :async, :password_expirable, :secure_validatable
|
||||
|
||||
acts_as_voter
|
||||
acts_as_paranoid column: :hidden_at
|
||||
|
||||
13
app/views/devise/password_expired/show.html.erb
Normal file
13
app/views/devise/password_expired/show.html.erb
Normal file
@@ -0,0 +1,13 @@
|
||||
<h2><%= t("devise.password_expired.expire_password") %></h2>
|
||||
|
||||
<%= form_for(resource, :as => resource_name, :url => [resource_name, :password_expired], :html => { :method => :put }) do |f| %>
|
||||
|
||||
<%= f.password_field :current_password %></p>
|
||||
|
||||
<%= f.label t("devise.password_expired.new_password") %>
|
||||
<%= f.password_field :password, label: false %></p>
|
||||
|
||||
<%= f.password_field :password_confirmation %></p>
|
||||
|
||||
<p><%= f.submit t("devise.password_expired.change_password") %></p>
|
||||
<% end %>
|
||||
@@ -101,6 +101,7 @@ ignore_missing:
|
||||
- 'errors.messages.taken'
|
||||
- 'devise.failure.invalid'
|
||||
- 'devise.registrations.destroyed'
|
||||
- 'devise.password_expired.*'
|
||||
|
||||
## Consider these keys used:
|
||||
ignore_unused:
|
||||
|
||||
71
config/initializers/devise_security_extension.rb
Normal file
71
config/initializers/devise_security_extension.rb
Normal file
@@ -0,0 +1,71 @@
|
||||
Devise.setup do |config|
|
||||
# ==> Security Extension
|
||||
# Configure security extension for devise
|
||||
|
||||
# Should the password expire (e.g 3.months)
|
||||
# config.expire_password_after = false
|
||||
config.expire_password_after = 1.year
|
||||
|
||||
# Need 1 char of A-Z, a-z and 0-9
|
||||
# config.password_regex = /(?=.*\\d)(?=.*[a-z])(?=.*[A-Z])/
|
||||
|
||||
# How many passwords to keep in archive
|
||||
#config.password_archiving_count = 5
|
||||
|
||||
# Deny old password (true, false, count)
|
||||
# config.deny_old_passwords = true
|
||||
|
||||
# enable email validation for :secure_validatable. (true, false, validation_options)
|
||||
# dependency: need an email validator like rails_email_validator
|
||||
# config.email_validation = true
|
||||
# captcha integration for recover form
|
||||
# config.captcha_for_recover = true
|
||||
|
||||
# captcha integration for sign up form
|
||||
# config.captcha_for_sign_up = true
|
||||
|
||||
# captcha integration for sign in form
|
||||
# config.captcha_for_sign_in = true
|
||||
|
||||
# captcha integration for unlock form
|
||||
# config.captcha_for_unlock = true
|
||||
|
||||
# captcha integration for confirmation form
|
||||
# config.captcha_for_confirmation = true
|
||||
|
||||
# Time period for account expiry from last_activity_at
|
||||
# config.expire_after = 90.days
|
||||
end
|
||||
|
||||
module Devise
|
||||
module Models
|
||||
module PasswordExpirable
|
||||
def need_change_password?
|
||||
self.administrator? && password_expired?
|
||||
end
|
||||
|
||||
def password_expired?
|
||||
self.password_changed_at < self.expire_password_after.ago
|
||||
end
|
||||
end
|
||||
|
||||
module SecureValidatable
|
||||
def self.included(base)
|
||||
base.extend ClassMethods
|
||||
assert_secure_validations_api!(base)
|
||||
base.class_eval do
|
||||
validate :current_equal_password_validation
|
||||
end
|
||||
end
|
||||
|
||||
def current_equal_password_validation
|
||||
if !self.new_record? && !self.encrypted_password_change.nil? && !self.erased?
|
||||
dummy = self.class.new
|
||||
dummy.encrypted_password = self.encrypted_password_change.first
|
||||
dummy.password_salt = self.password_salt_change.first if self.respond_to? :password_salt_change and not self.password_salt_change.nil?
|
||||
self.errors.add(:password, :equal_to_current_password) if dummy.valid_password?(self.password)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -54,6 +54,7 @@ en:
|
||||
username: "Username"
|
||||
password_confirmation: "Password confirmation"
|
||||
password: "Password"
|
||||
current_password: "Current password"
|
||||
phone_number: "Phone number"
|
||||
official_position: "Official position"
|
||||
official_level: "Official level"
|
||||
|
||||
@@ -54,6 +54,7 @@ es:
|
||||
username: "Nombre de usuario"
|
||||
password_confirmation: "Confirmación de contraseña"
|
||||
password: "Contraseña"
|
||||
current_password: "Contraseña actual"
|
||||
phone_number: "Teléfono"
|
||||
official_position: "Cargo público"
|
||||
official_level: "Nivel del cargo"
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
|
||||
en:
|
||||
devise:
|
||||
password_expired:
|
||||
expire_password: "Password expired"
|
||||
change_required: "Your password is expired"
|
||||
change_password: "Change your password"
|
||||
new_password: "New password"
|
||||
updated: "Password successfully updated"
|
||||
confirmations:
|
||||
confirmed: "Your account has been confirmed."
|
||||
send_instructions: "In a few minutes you will receive an email containing instructions on how to reset your password."
|
||||
@@ -62,3 +68,4 @@ en:
|
||||
not_saved:
|
||||
one: "1 error prevented this %{resource} from being saved:"
|
||||
other: "%{count} errors prevented this %{resource} from being saved:"
|
||||
equal_to_current_password: "must be different than the current password."
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
es:
|
||||
devise:
|
||||
password_expired:
|
||||
expire_password: "Contraseña caducada"
|
||||
change_required: "Tu contraseña ha caducado"
|
||||
change_password: "Cambia tu contraseña"
|
||||
new_password: "Nueva contraseña"
|
||||
updated: "Contraseña actualizada con éxito"
|
||||
confirmations:
|
||||
confirmed: "Tu cuenta ha sido confirmada. Por favor autentifícate con tu red social o tu usuario y contraseña"
|
||||
send_instructions: "Recibirás un correo electrónico en unos minutos con instrucciones sobre cómo restablecer tu contraseña."
|
||||
@@ -60,3 +66,4 @@ es:
|
||||
not_saved:
|
||||
one: "1 error impidió que este %{resource} fuera guardado:"
|
||||
other: "%{count} errores impidieron que este %{resource} fuera guardado:"
|
||||
equal_to_current_password: "debe ser diferente a la contraseña actual"
|
||||
|
||||
6
db/migrate/20160901104320_add_password_expired.rb
Normal file
6
db/migrate/20160901104320_add_password_expired.rb
Normal file
@@ -0,0 +1,6 @@
|
||||
class AddPasswordExpired < ActiveRecord::Migration
|
||||
def change
|
||||
add_column :users, :password_changed_at, :datetime
|
||||
add_index :users, :password_changed_at
|
||||
end
|
||||
end
|
||||
@@ -11,7 +11,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 20160803154011) do
|
||||
ActiveRecord::Schema.define(version: 20160901104320) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
@@ -464,12 +464,14 @@ ActiveRecord::Schema.define(version: 20160803154011) do
|
||||
t.boolean "email_digest", default: true
|
||||
t.boolean "email_on_direct_message", default: true
|
||||
t.boolean "official_position_badge", default: false
|
||||
t.datetime "password_changed_at"
|
||||
end
|
||||
|
||||
add_index "users", ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree
|
||||
add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree
|
||||
add_index "users", ["geozone_id"], name: "index_users_on_geozone_id", using: :btree
|
||||
add_index "users", ["hidden_at"], name: "index_users_on_hidden_at", using: :btree
|
||||
add_index "users", ["password_changed_at"], name: "index_users_on_password_changed_at", using: :btree
|
||||
add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree
|
||||
add_index "users", ["username"], name: "index_users_on_username", using: :btree
|
||||
|
||||
|
||||
@@ -76,4 +76,12 @@ namespace :users do
|
||||
task remove_erased_identities: :environment do
|
||||
Identity.joins(:user).where('users.erased_at IS NOT NULL').destroy_all
|
||||
end
|
||||
|
||||
desc "Update password changed at for existing users"
|
||||
task update_password_changed_at: :environment do
|
||||
User.all.each do |user|
|
||||
user.update(password_changed_at: user.created_at)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -285,4 +285,59 @@ feature 'Users' do
|
||||
|
||||
expect(page).to have_content "Your password has been changed successfully."
|
||||
end
|
||||
|
||||
scenario 'Sign in, admin with password expired' do
|
||||
user = create(:user, password_changed_at: Time.now - 1.year)
|
||||
admin = create(:administrator, user: user)
|
||||
|
||||
login_as(admin.user)
|
||||
visit root_path
|
||||
|
||||
expect(page).to have_content "Your password is expired"
|
||||
|
||||
fill_in 'user_current_password', with: 'judgmentday'
|
||||
fill_in 'user_password', with: '123456789'
|
||||
fill_in 'user_password_confirmation', with: '123456789'
|
||||
|
||||
click_button 'Change your password'
|
||||
|
||||
expect(page).to have_content "Password successfully updated"
|
||||
end
|
||||
|
||||
scenario 'Sign in, admin without password expired' do
|
||||
user = create(:user, password_changed_at: Time.now - 360.days)
|
||||
admin = create(:administrator, user: user)
|
||||
|
||||
login_as(admin.user)
|
||||
visit root_path
|
||||
|
||||
expect(page).to_not have_content "Your password is expired"
|
||||
end
|
||||
|
||||
scenario 'Sign in, user with password expired' do
|
||||
user = create(:user, password_changed_at: Time.now - 1.year)
|
||||
|
||||
login_as(user)
|
||||
visit root_path
|
||||
|
||||
expect(page).to_not have_content "Your password is expired"
|
||||
end
|
||||
|
||||
scenario 'Admin with password expired trying to use same password' do
|
||||
user = create(:user, password_changed_at: Time.now - 1.year, password: '123456789')
|
||||
admin = create(:administrator, user: user)
|
||||
|
||||
login_as(admin.user)
|
||||
visit root_path
|
||||
|
||||
expect(page).to have_content "Your password is expired"
|
||||
|
||||
fill_in 'user_current_password', with: 'judgmentday'
|
||||
fill_in 'user_password', with: '123456789'
|
||||
fill_in 'user_password_confirmation', with: '123456789'
|
||||
click_button 'Change your password'
|
||||
|
||||
expect(page).to have_content "must be different than the current password."
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user