Merge pull request #5591 from betagouv/dev

2020-09-21-01
This commit is contained in:
Keirua 2020-09-21 10:02:39 +02:00 committed by GitHub
commit dc39df1edc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 206 additions and 84 deletions

View file

@ -102,7 +102,7 @@ GEM
rake (>= 10.4, < 14.0)
ast (2.4.1)
attr_required (1.0.1)
autoprefixer-rails (10.0.0.1)
autoprefixer-rails (10.0.0.2)
execjs
axe-matchers (2.6.1)
dumb_delegator (~> 0.8)
@ -138,7 +138,7 @@ GEM
rack-test (>= 0.6.3)
regexp_parser (~> 1.5)
xpath (~> 3.2)
capybara-email (3.0.1)
capybara-email (3.0.2)
capybara (>= 2.4, < 4.0)
mail
capybara-screenshot (1.0.24)
@ -159,7 +159,7 @@ GEM
choice (0.2.0)
chunky_png (1.3.11)
clamav-client (3.2.0)
coderay (1.1.2)
coderay (1.1.3)
coercible (1.0.0)
descendants_tracker (~> 0.0.1)
coffee-rails (5.0.0)
@ -171,8 +171,7 @@ GEM
coffee-script-source (1.12.2)
concurrent-ruby (1.1.7)
connection_pool (2.2.3)
crack (0.4.3)
safe_yaml (~> 1.0.0)
crack (0.4.4)
crass (1.0.6)
css_parser (1.7.1)
addressable
@ -289,7 +288,7 @@ GEM
thor (>= 0.19, < 2.0)
graphql_playground-rails (2.1.0)
rails (>= 5.1.0)
groupdate (5.0.0)
groupdate (5.2.1)
activesupport (>= 5)
guard (2.16.2)
formatador (>= 0.2.4)
@ -467,7 +466,7 @@ GEM
prawn (>= 0.11.1, < 3)
prawn-table (0.2.2)
prawn (>= 1.3.0, < 3.0.0)
premailer (1.11.1)
premailer (1.13.1)
addressable
css_parser (>= 1.6.0)
htmlentities (>= 4.0.0)
@ -488,9 +487,9 @@ GEM
activesupport (>= 3.0.0)
raabro (1.1.6)
rack (2.2.3)
rack-attack (6.2.2)
rack-attack (6.3.1)
rack (>= 1.0, < 3)
rack-mini-profiler (2.0.1)
rack-mini-profiler (2.1.0)
rack (>= 1.2.0)
rack-oauth2 (1.15.0)
activesupport
@ -624,10 +623,9 @@ GEM
rexml
ruby-progressbar (1.10.1)
ruby2_keywords (0.0.2)
ruby_parser (3.14.2)
ruby_parser (3.15.0)
sexp_processor (~> 4.9)
rubyzip (2.3.0)
safe_yaml (1.0.5)
sanitize-url (0.1.4)
sass (3.7.4)
sass-listen (~> 4.0.0)
@ -649,11 +647,11 @@ GEM
childprocess (>= 0.5, < 4.0)
rubyzip (>= 1.2.2)
semantic_range (2.3.0)
sentry-raven (3.0.0)
sentry-raven (3.1.0)
faraday (>= 1.0)
sexp_processor (4.14.1)
sexp_processor (4.15.1)
shellany (0.0.1)
shoulda-matchers (4.3.0)
shoulda-matchers (4.4.1)
activesupport (>= 4.2.0)
simple_xlsx_reader (1.0.4)
nokogiri
@ -676,7 +674,7 @@ GEM
axlsx_styler (>= 1.0.0, < 2)
caxlsx (>= 2.0.2, < 4)
rodf (>= 1.0.0, < 2)
spring (2.1.0)
spring (2.1.1)
spring-commands-rspec (1.0.4)
spring (>= 0.9.1)
sprockets (3.7.2)
@ -697,7 +695,7 @@ GEM
tilt (2.0.10)
timecop (0.9.1)
ttfunk (1.6.2.1)
typhoeus (1.3.1)
typhoeus (1.4.0)
ethon (>= 0.9.0)
tzinfo (1.2.7)
thread_safe (~> 0.1)
@ -708,10 +706,10 @@ GEM
validate_email (0.1.6)
activemodel (>= 3.0)
mail (>= 2.2.5)
validate_url (1.0.8)
validate_url (1.0.11)
activemodel (>= 3.0.0)
public_suffix
vcr (5.1.0)
vcr (6.0.0)
virtus (1.0.5)
axiom-types (~> 0.1)
coercible (~> 1.0)
@ -719,19 +717,19 @@ GEM
equalizer (~> 0.0, >= 0.0.9)
warden (1.2.9)
rack (>= 2.0.9)
web-console (4.0.1)
web-console (4.0.4)
actionview (>= 6.0.0)
activemodel (>= 6.0.0)
bindex (>= 0.4.0)
railties (>= 6.0.0)
webdrivers (4.2.0)
webdrivers (4.4.1)
nokogiri (~> 1.6)
rubyzip (>= 1.3.0)
selenium-webdriver (>= 3.0, < 4.0)
webfinger (1.1.0)
activesupport
httpclient (>= 2.4)
webmock (3.8.3)
webmock (3.9.1)
addressable (>= 2.3.6)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
@ -752,7 +750,7 @@ GEM
zipline (1.1.1)
rails (>= 3.2.1, < 6.1)
zip_tricks (>= 4.2.1, <= 5.0.0)
zxcvbn-ruby (1.0.0)
zxcvbn-ruby (1.1.0)
PLATFORMS
ruby

View file

@ -1291,6 +1291,11 @@ enum TypeDeChamp {
"""
textarea
"""
Titre identité
"""
titre_identite
"""
Oui/Non
"""

View file

@ -26,6 +26,7 @@ const TypeDeChamp = sortableElement(
const isCarte = typeDeChamp.type_champ === 'carte';
const isExplication = typeDeChamp.type_champ === 'explication';
const isHeaderSection = typeDeChamp.type_champ === 'header_section';
const isTitreIdentite = typeDeChamp.type_champ === 'titre_identite';
const isRepetition = typeDeChamp.type_champ === 'repetition';
const canBeMandatory =
!isHeaderSection && !isExplication && !state.isAnnotation;
@ -118,7 +119,7 @@ const TypeDeChamp = sortableElement(
</div>
<div className="flex justify-start">
<DescriptionInput
isVisible={!isHeaderSection}
isVisible={!isHeaderSection && !isTitreIdentite}
handler={updateHandlers.description}
/>
</div>

View file

@ -2,13 +2,13 @@
class AdministrationMailer < ApplicationMailer
layout 'mailers/layout'
def invite_admin(admin, reset_password_token, administration_id)
def invite_admin(user, reset_password_token, administration_id)
@reset_password_token = reset_password_token
@admin = admin
@user = user
@author_name = BizDev.full_name(administration_id)
subject = "Activez votre compte administrateur"
mail(to: admin.email,
mail(to: user.email,
subject: subject,
reply_to: CONTACT_EMAIL)
end

View file

@ -31,14 +31,6 @@ class Administrateur < ApplicationRecord
user&.email
end
# validate :password_complexity, if: Proc.new { |a| Devise.password_length.include?(a.password.try(:size)) }
def password_complexity
if password.present? && ZxcvbnService.new(password).score < PASSWORD_COMPLEXITY_FOR_ADMIN
errors.add(:password, :not_strong)
end
end
def self.find_inactive_by_token(reset_password_token)
self.inactive.with_reset_password_token(reset_password_token)
end

View file

@ -0,0 +1,57 @@
# == Schema Information
#
# Table name: champs
#
# id :integer not null, primary key
# private :boolean default(FALSE), not null
# row :integer
# type :string
# value :string
# created_at :datetime
# updated_at :datetime
# dossier_id :integer
# etablissement_id :integer
# parent_id :bigint
# type_de_champ_id :integer
#
class Champs::TitreIdentiteChamp < Champ
MAX_SIZE = 20.megabytes
ACCEPTED_FORMATS = [
"image/png",
"image/jpeg"
]
# TODO: once we're running on Rails 6, re-enable this validation.
# See https://github.com/betagouv/demarches-simplifiees.fr/issues/4926
#
validates :piece_justificative_file,
content_type: ACCEPTED_FORMATS,
size: { less_than: MAX_SIZE }
def main_value_name
:piece_justificative_file
end
def search_terms
# We dont know how to search inside documents yet
end
def mandatory_and_blank?
mandatory? && !piece_justificative_file.attached?
end
def for_export
piece_justificative_file.filename.to_s if piece_justificative_file.attached?
end
def for_api
if piece_justificative_file.attached? && (piece_justificative_file.virus_scanner.safe? || piece_justificative_file.virus_scanner.pending?)
piece_justificative_file.service_url
end
end
def update_skip_pj_validation
type_de_champ.update(skip_pj_validation: true)
end
end

View file

@ -47,7 +47,8 @@ class TypeDeChamp < ApplicationRecord
piece_justificative: 'piece_justificative',
siret: 'siret',
carte: 'carte',
repetition: 'repetition'
repetition: 'repetition',
titre_identite: 'titre_identite'
}
belongs_to :revision, class_name: 'ProcedureRevision', optional: true
@ -189,7 +190,7 @@ class TypeDeChamp < ApplicationRecord
end
def piece_justificative?
type_champ == TypeDeChamp.type_champs.fetch(:piece_justificative)
type_champ == TypeDeChamp.type_champs.fetch(:piece_justificative) || type_champ == TypeDeChamp.type_champs.fetch(:titre_identite)
end
def legacy_number?

View file

@ -0,0 +1,2 @@
class TypesDeChamp::TitreIdentiteTypeDeChamp < TypesDeChamp::TypeDeChampBase
end

View file

@ -54,6 +54,14 @@ class User < ApplicationRecord
before_validation -> { sanitize_email(:email) }
validate :password_complexity, 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
def send_confirmation_instructions
unless @raw_confirmation_token
@ -90,13 +98,7 @@ class User < ApplicationRecord
end
def invite_administrateur!(administration_id)
reset_password_token = nil
if !active?
reset_password_token = set_reset_password_token
end
AdministrationMailer.invite_admin(self, reset_password_token, administration_id).deliver_later
AdministrationMailer.invite_admin(self, set_reset_password_token, administration_id).deliver_later
end
def remind_invitation!

View file

@ -7,17 +7,18 @@
Je vous remercie de lintérêt que vous portez à notre outil de dématérialisation de démarches.
%p
Votre compte administrateur a été créé pour l'adresse email #{@admin.email}.
Votre compte administrateur a été créé pour l'adresse email #{@user.email}.
- if @reset_password_token.present?
- if !@user.active?
%p
%b
Pour lactiver, cliquez sur le lien suivant :
= link_to(admin_activate_url(token: @reset_password_token), admin_activate_url(token: @reset_password_token))
- else
%p
%b
Pour vous connecter, cliquez sur le lien suivant :
= link_to(new_user_session_url, new_user_session_url)
= link_to edit_user_password_url(@user, reset_password_token: @reset_password_token), edit_user_password_url(@user, reset_password_token: @reset_password_token)
%p
= render partial: "layouts/mailers/bizdev_signature", locals: { author_name: @author_name }

View file

@ -26,6 +26,8 @@
= render partial: "shared/champs/multiple_drop_down_list/show", locals: { champ: c }
- when TypeDeChamp.type_champs.fetch(:piece_justificative)
= render partial: "shared/champs/piece_justificative/show", locals: { champ: c }
- when TypeDeChamp.type_champs.fetch(:titre_identite)
= render partial: "shared/champs/piece_justificative/show", locals: { champ: c }
- when TypeDeChamp.type_champs.fetch(:siret)
= render partial: "shared/champs/siret/show", locals: { champ: c, profile: profile }
- when TypeDeChamp.type_champs.fetch(:textarea)

View file

@ -6,6 +6,8 @@
- elsif has_label?(champ)
= render partial: 'shared/dossiers/editable_champs/champ_label', locals: { form: form, champ: champ, seen_at: defined?(seen_at) ? seen_at : nil }
- if champ.type_champ == "titre_identite"
%p.notice Carte d'identité (uniquement le recto), passeport ou titre de séjour. Formats acceptés : jpg / png.
= render partial: "shared/dossiers/editable_champs/#{champ.type_champ}",
locals: { champ: champ, form: form }

View file

@ -0,0 +1,4 @@
= render 'shared/attachment/edit',
{ form: form,
attached_file: champ.piece_justificative_file,
template: champ.type_de_champ.piece_justificative_template, user_can_destroy: true }

View file

@ -82,6 +82,7 @@ fr:
taken: déjà utilisé
password:
too_short: 'est trop court'
not_strong: 'nest pas assez complexe'
password_confirmation:
confirmation: ': Les deux mots de passe ne correspondent pas'
invite:

View file

@ -33,3 +33,4 @@ fr:
siret: 'SIRET'
carte: 'Carte'
repetition: 'Bloc répétable'
titre_identite: 'Titre identité'

View file

@ -25,7 +25,7 @@ describe WebhookController, type: :controller do
end
context 'when there is a matching user' do
let(:user) { create(:user) }
let(:user) { create(:user, :with_strong_password) }
let(:customer_email) { user.email }
it 'returns a 200 response' do

View file

@ -3,7 +3,7 @@ FactoryBot.define do
factory :administrateur do
transient do
email { generate(:administrateur_email) }
password { 'mon chien aime les bananes' }
password { 'Mon [hien 4im3 {es banane$' }
end
initialize_with do

View file

@ -143,6 +143,14 @@ FactoryBot.define do
end
end
factory :champ_titre_identite, class: 'Champs::TitreIdentiteChamp' do
type_de_champ { association :type_de_champ_titre_identite, procedure: dossier.procedure }
after(:build) do |champ, _evaluator|
champ.piece_justificative_file.attach(io: StringIO.new("toto"), filename: "toto.png", content_type: "image/png")
end
end
factory :champ_carte, class: 'Champs::CarteChamp' do
type_de_champ { association :type_de_champ_carte, procedure: dossier.procedure }
end

View file

@ -133,6 +133,9 @@ FactoryBot.define do
type_de_champ.piece_justificative_template.attach(io: StringIO.new("toto"), filename: "toto.txt", content_type: "text/plain")
end
end
factory :type_de_champ_titre_identite do
type_champ { TypeDeChamp.type_champs.fetch(:titre_identite) }
end
factory :type_de_champ_siret do
type_champ { TypeDeChamp.type_champs.fetch(:siret) }
end

View file

@ -8,5 +8,9 @@ FactoryBot.define do
trait :unconfirmed do
confirmed_at { nil }
end
trait :with_strong_password do
password { '{my-%s3cure[]-p4$$w0rd' }
end
end
end

View file

@ -1,12 +1,24 @@
RSpec.describe AdministrationMailer, type: :mailer do
describe '#invite_admin' do
let(:admin) { create(:administrateur) }
let(:token) { "Toc toc toc" }
let(:admin_user) { create(:user, last_sign_in_at: last_sign_in_at) }
let(:token) { "some_token" }
let(:administration_id) { BizDev::PIPEDRIVE_ID }
let(:last_sign_in_at) { nil }
subject { described_class.invite_admin(admin, token, administration_id) }
subject { described_class.invite_admin(admin_user, token, administration_id) }
it { expect(subject.subject).not_to be_empty }
describe "when the user has not been activated" do
it { expect(subject.body).to include(admin_activate_path(token: token)) }
it { expect(subject.body).not_to include(edit_user_password_url(admin_user, reset_password_token: token)) }
end
describe "when the user is already active" do
let(:last_sign_in_at) { Time.zone.now }
it { expect(subject.body).not_to include(admin_activate_path(token: token)) }
it { expect(subject.body).to include(edit_user_password_url(admin_user, reset_password_token: token)) }
end
end
describe '#refuse_admin' do

View file

@ -75,34 +75,4 @@ describe Administrateur, type: :model do
expect(Administrateur.find_by(id: administrateur.id)).to be_nil
end
end
# describe '#password_complexity' do
# let(:email) { 'mail@beta.gouv.fr' }
# let(:passwords) { ['pass', '12pass23', 'démarches ', 'démarches-simple', 'my-s3cure-p4ssword'] }
# let(:administrateur) { build(:administrateur, email: email, password: password) }
# let(:min_complexity) { PASSWORD_COMPLEXITY_FOR_ADMIN }
# subject do
# administrateur.save
# administrateur.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
# let(:password) { passwords[min_complexity - 1] }
# it { expect(subject).to eq(["Le mot de passe n'est pas assez complexe"]) }
# end
# context 'when password is acceptable' do
# let(:password) { passwords[min_complexity] }
# it { expect(subject).to eq([]) }
# end
# end
end

View file

@ -186,7 +186,9 @@ describe User, type: :model do
subject
end
it { expect(AdministrationMailer).to have_received(:invite_admin).with(user, nil, administration.id) }
it 'receives an invitation to update its password' do
expect(AdministrationMailer).to have_received(:invite_admin).with(user, kind_of(String), administration.id)
end
end
end
@ -293,4 +295,55 @@ describe User, type: :model do
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
context 'administrateurs' do
let(:email) { 'mail@beta.gouv.fr' }
let(:administrateur) { build(:user, email: email, password: password, administrateur: build(:administrateur)) }
subject do
administrateur.save
administrateur.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 nest pas assez complexe"]) }
end
end
context 'when password is acceptable' do
let(:password) { passwords[min_complexity] }
it { expect(subject).to eq([]) }
end
end
context 'simple users' do
passwords.each do |password|
let(:user) { build(:user, email: 'some@email.fr', password: password) }
it 'has no complexity validation' do
user.save
expect(user.errors.full_messages).to eq([])
end
end
end
end
end

View file

@ -36,7 +36,7 @@ describe AdministrateurUsageStatisticsService do
context 'for an administrateur that has plenty of things' do
let(:administrateur) do
create(:administrateur,
user: create(:user, sign_in_count: 17, current_sign_in_at: Time.zone.local(2019, 3, 7), last_sign_in_at: Time.zone.local(2019, 2, 27)),
user: create(:user, :with_strong_password, sign_in_count: 17, current_sign_in_at: Time.zone.local(2019, 3, 7), last_sign_in_at: Time.zone.local(2019, 2, 27)),
services: [create(:service)],
instructeurs: [create(:instructeur)])
end

View file

@ -73,6 +73,7 @@ describe ProcedureExportService do
"piece_justificative",
"siret",
"carte",
"titre_identite",
"text"
]
end
@ -156,6 +157,7 @@ describe ProcedureExportService do
"piece_justificative",
"siret",
"carte",
"titre_identite",
"text"
]
end
@ -235,6 +237,7 @@ describe ProcedureExportService do
"piece_justificative",
"siret",
"carte",
"titre_identite",
"text"
]
end