Merge pull request #5749 from betagouv/5746-super_admin_enforce_password
5746 super admin enforce password
This commit is contained in:
commit
e380440100
10 changed files with 93 additions and 15 deletions
|
@ -3,4 +3,17 @@ class SuperAdmins::PasswordsController < Devise::PasswordsController
|
||||||
super
|
super
|
||||||
self.resource.disable_otp!
|
self.resource.disable_otp!
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_strength
|
||||||
|
@score, @words, @length = ZxcvbnService.new(password_params[:password]).complexity
|
||||||
|
@min_length = PASSWORD_MIN_LENGTH
|
||||||
|
@min_complexity = PASSWORD_COMPLEXITY_FOR_ADMIN
|
||||||
|
render 'shared/password/test_strength'
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def password_params
|
||||||
|
params.require(:super_admin).permit(:password)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -28,6 +28,8 @@ class SuperAdmin < ApplicationRecord
|
||||||
devise :rememberable, :trackable, :validatable, :lockable, :async, :recoverable,
|
devise :rememberable, :trackable, :validatable, :lockable, :async, :recoverable,
|
||||||
:two_factor_authenticatable, :otp_secret_encryption_key => Rails.application.secrets.otp_secret_key
|
:two_factor_authenticatable, :otp_secret_encryption_key => Rails.application.secrets.otp_secret_key
|
||||||
|
|
||||||
|
validates :password, password_complexity: true, if: -> (u) { Devise.password_length.include?(u.password.try(:size)) }
|
||||||
|
|
||||||
def enable_otp!
|
def enable_otp!
|
||||||
self.otp_secret = SuperAdmin.generate_otp_secret
|
self.otp_secret = SuperAdmin.generate_otp_secret
|
||||||
self.otp_required_for_login = true
|
self.otp_required_for_login = true
|
||||||
|
|
|
@ -54,13 +54,7 @@ class User < ApplicationRecord
|
||||||
|
|
||||||
before_validation -> { sanitize_email(:email) }
|
before_validation -> { sanitize_email(:email) }
|
||||||
|
|
||||||
validate :password_complexity, if: -> (u) { u.administrateur.present? && Devise.password_length.include?(u.password.try(:size)) }
|
validates :password, password_complexity: true, if: -> (u) { u.administrateur.present? && Devise.password_length.include?(u.password.try(:size)) }
|
||||||
|
|
||||||
def password_complexity
|
|
||||||
if password.present? && ZxcvbnService.new(password).score < PASSWORD_COMPLEXITY_FOR_ADMIN
|
|
||||||
errors.add(:password, :not_strong)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Override of Devise::Models::Confirmable#send_confirmation_instructions
|
# Override of Devise::Models::Confirmable#send_confirmation_instructions
|
||||||
def send_confirmation_instructions
|
def send_confirmation_instructions
|
||||||
|
|
7
app/validators/password_complexity_validator.rb
Normal file
7
app/validators/password_complexity_validator.rb
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
class PasswordComplexityValidator < ActiveModel::EachValidator
|
||||||
|
def validate_each(record, attribute, value)
|
||||||
|
if value.present? && ZxcvbnService.new(value).score < PASSWORD_COMPLEXITY_FOR_ADMIN
|
||||||
|
record.errors.add(attribute, :not_strong)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -14,9 +14,8 @@
|
||||||
= f.hidden_field :reset_password_token
|
= f.hidden_field :reset_password_token
|
||||||
|
|
||||||
= f.label 'Nouveau mot de passe'
|
= f.label 'Nouveau mot de passe'
|
||||||
= f.password_field :password, autofocus: true, autocomplete: 'off'
|
|
||||||
|
|
||||||
= f.label 'Confirmez le nouveau mot de passe'
|
= render partial: 'shared/password/edit_password', locals: { form: f, controller: 'super_admins/passwords' }
|
||||||
= f.password_field :password_confirmation, autocomplete: 'off'
|
|
||||||
|
|
||||||
= f.submit 'Changer le mot de passe', class: 'button primary'
|
|
||||||
|
= f.submit 'Changer le mot de passe', class: 'button large primary expand', id: "submit-password", data: { disable_with: "Envoi..." }
|
||||||
|
|
|
@ -60,11 +60,15 @@ fr:
|
||||||
|
|
||||||
activerecord:
|
activerecord:
|
||||||
attributes:
|
attributes:
|
||||||
|
default_attributes: &default_attributes
|
||||||
|
password: 'Le mot de passe'
|
||||||
user:
|
user:
|
||||||
siret: 'Numéro SIRET'
|
siret: 'Numéro SIRET'
|
||||||
password: 'Le mot de passe'
|
<< : *default_attributes
|
||||||
instructeur:
|
instructeur:
|
||||||
password: 'Le mot de passe'
|
<< : *default_attributes
|
||||||
|
super_admin:
|
||||||
|
<< : *default_attributes
|
||||||
errors:
|
errors:
|
||||||
messages:
|
messages:
|
||||||
not_a_phone: 'Numéro de téléphone invalide'
|
not_a_phone: 'Numéro de téléphone invalide'
|
||||||
|
@ -80,7 +84,7 @@ fr:
|
||||||
email:
|
email:
|
||||||
invalid: invalide
|
invalid: invalide
|
||||||
taken: déjà utilisé
|
taken: déjà utilisé
|
||||||
password:
|
password: &password
|
||||||
too_short: 'est trop court'
|
too_short: 'est trop court'
|
||||||
not_strong: 'n’est pas assez complexe'
|
not_strong: 'n’est pas assez complexe'
|
||||||
password_confirmation:
|
password_confirmation:
|
||||||
|
@ -96,6 +100,10 @@ fr:
|
||||||
taken: déjà utilisé
|
taken: déjà utilisé
|
||||||
password:
|
password:
|
||||||
too_short: 'est trop court'
|
too_short: 'est trop court'
|
||||||
|
super_admin:
|
||||||
|
attributes:
|
||||||
|
password:
|
||||||
|
<< : *password
|
||||||
procedure:
|
procedure:
|
||||||
attributes:
|
attributes:
|
||||||
path:
|
path:
|
||||||
|
|
|
@ -84,6 +84,10 @@ Rails.application.routes.draw do
|
||||||
passwords: 'super_admins/passwords'
|
passwords: 'super_admins/passwords'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
devise_scope :super_admin do
|
||||||
|
get '/super_admins/password/test_strength' => 'super_admins/passwords#test_strength'
|
||||||
|
end
|
||||||
|
|
||||||
get 'super_admins/edit_otp', to: 'super_admins#edit_otp', as: 'edit_super_admin_otp'
|
get 'super_admins/edit_otp', to: 'super_admins#edit_otp', as: 'edit_super_admin_otp'
|
||||||
put 'super_admins/enable_otp', to: 'super_admins#enable_otp', as: 'enable_super_admin_otp'
|
put 'super_admins/enable_otp', to: 'super_admins#enable_otp', as: 'enable_super_admin_otp'
|
||||||
|
|
||||||
|
|
12
spec/controllers/super_admins/passwords_controller_spec.rb
Normal file
12
spec/controllers/super_admins/passwords_controller_spec.rb
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
describe SuperAdmins::PasswordsController, type: :controller do
|
||||||
|
describe '#test_strength' do
|
||||||
|
it 'calculate score' do
|
||||||
|
password = "bonjour"
|
||||||
|
@request.env["devise.mapping"] = Devise.mappings[:super_admin]
|
||||||
|
|
||||||
|
get 'test_strength', xhr: true, params: { super_admin: { password: password } }
|
||||||
|
|
||||||
|
expect(assigns(:score)).to be_present
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -2,7 +2,7 @@ FactoryBot.define do
|
||||||
sequence(:super_admin_email) { |n| "plop#{n}@plop.com" }
|
sequence(:super_admin_email) { |n| "plop#{n}@plop.com" }
|
||||||
factory :super_admin do
|
factory :super_admin do
|
||||||
email { generate(:super_admin_email) }
|
email { generate(:super_admin_email) }
|
||||||
password { 'my-s3cure-p4ssword' }
|
password { '{My-$3cure-p4ssWord}' }
|
||||||
otp_required_for_login { true }
|
otp_required_for_login { true }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -61,4 +61,43 @@ describe SuperAdmin, type: :model do
|
||||||
expect { subject }.to change { super_admin.reload.otp_secret }.to(nil)
|
expect { subject }.to change { super_admin.reload.otp_secret }.to(nil)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#password_complexity' do
|
||||||
|
# This password list is sorted by password complexity, according to zxcvbn (used for complexity evaluation)
|
||||||
|
# 0 - too guessable: risky password. (guesses < 10^3)
|
||||||
|
# 1 - very guessable: protection from throttled online attacks. (guesses < 10^6)
|
||||||
|
# 2 - somewhat guessable: protection from unthrottled online attacks. (guesses < 10^8)
|
||||||
|
# 3 - safely unguessable: moderate protection from offline slow-hash scenario. (guesses < 10^10)
|
||||||
|
# 4 - very unguessable: strong protection from offline slow-hash scenario. (guesses >= 10^10)
|
||||||
|
passwords = ['pass', '12pass23', 'démarches ', 'démarches-simple', '{My-$3cure-p4ssWord}']
|
||||||
|
min_complexity = PASSWORD_COMPLEXITY_FOR_ADMIN
|
||||||
|
|
||||||
|
let(:email) { 'mail@beta.gouv.fr' }
|
||||||
|
let(:super_admin) { build(:super_admin, email: email, password: password) }
|
||||||
|
|
||||||
|
subject do
|
||||||
|
super_admin.save
|
||||||
|
super_admin.errors.full_messages
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when password is too short' do
|
||||||
|
let(:password) { 's' * (PASSWORD_MIN_LENGTH - 1) }
|
||||||
|
|
||||||
|
it { expect(subject).to eq(["Le mot de passe est trop court"]) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when password is too simple' do
|
||||||
|
passwords[0..(min_complexity - 1)].each do |password|
|
||||||
|
let(:password) { password }
|
||||||
|
|
||||||
|
it { expect(subject).to eq(["Le mot de passe n’est pas assez complexe"]) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when password is acceptable' do
|
||||||
|
let(:password) { passwords[min_complexity] }
|
||||||
|
|
||||||
|
it { expect(subject).to eq([]) }
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue