diff --git a/Gemfile b/Gemfile index 020990ae5..17d37eee9 100644 --- a/Gemfile +++ b/Gemfile @@ -48,6 +48,7 @@ gem 'prawn_rails' gem 'premailer-rails' gem 'puma' # Use Puma as the app server gem 'pundit' +gem 'rack-attack' gem 'rack-mini-profiler' gem 'rails' gem 'rails-i18n' # Locales par défaut diff --git a/Gemfile.lock b/Gemfile.lock index 055c310e9..0f27aff81 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -434,6 +434,8 @@ GEM pundit (2.0.1) activesupport (>= 3.0.0) rack (2.0.6) + rack-attack (6.0.0) + rack (>= 1.0, < 3) rack-mini-profiler (1.0.1) rack (>= 1.2.0) rack-oauth2 (1.9.3) @@ -752,6 +754,7 @@ DEPENDENCIES pry-byebug puma pundit + rack-attack rack-mini-profiler rails rails-controller-testing diff --git a/app/controllers/admin/instructeurs_controller.rb b/app/controllers/admin/instructeurs_controller.rb index 37bf436e4..0cacc9bc2 100644 --- a/app/controllers/admin/instructeurs_controller.rb +++ b/app/controllers/admin/instructeurs_controller.rb @@ -37,21 +37,15 @@ class Admin::InstructeursController < AdminController private def invite_instructeur(email) - user = User.find_by(email: email) - - if user.nil? - user = User.create( - email: email, - password: SecureRandom.hex, - confirmed_at: Time.zone.now - ) - end - - if user.errors.empty? - @instructeur = Instructeur.create(email: email, administrateurs: [current_administrateur]) - user.update!(instructeur: @instructeur) + user = User.create_or_promote_to_instructeur( + email, + SecureRandom.hex, + administrateurs: [current_administrateur] + ) + if user.valid? user.invite! + flash.notice = 'Instructeur ajouté' else flash.alert = user.errors.full_messages diff --git a/app/controllers/administrateurs/activate_controller.rb b/app/controllers/administrateurs/activate_controller.rb index 2d45c0da1..b4bf8346d 100644 --- a/app/controllers/administrateurs/activate_controller.rb +++ b/app/controllers/administrateurs/activate_controller.rb @@ -47,7 +47,6 @@ class Administrateurs::ActivateController < ApplicationController if resource&.valid_password?(password) sign_in resource - resource.force_sync_credentials end end end diff --git a/app/controllers/instructeurs/avis_controller.rb b/app/controllers/instructeurs/avis_controller.rb index 4f2f2465a..2cff98e59 100644 --- a/app/controllers/instructeurs/avis_controller.rb +++ b/app/controllers/instructeurs/avis_controller.rb @@ -83,23 +83,13 @@ module Instructeurs email = params[:email] password = params['instructeur']['password'] - user = User.find_by(email: email) - - if user.nil? - user = User.create( - email: email, - password: password, - confirmed_at: Time.zone.now - ) - end - - if user.errors.empty? - instructeur = Instructeur.create(email: email) - user.update!(instructeur: instructeur) + # Not perfect because the password will not be changed if the user already exists + user = User.create_or_promote_to_instructeur(email, password) + if user.valid? sign_in(user) - Avis.link_avis_to_instructeur(instructeur) + Avis.link_avis_to_instructeur(user.instructeur) redirect_to url_for(instructeur_avis_index_path) else flash[:alert] = user.errors.full_messages diff --git a/app/controllers/manager/administrateurs_controller.rb b/app/controllers/manager/administrateurs_controller.rb index 83894c0ab..74bd191ea 100644 --- a/app/controllers/manager/administrateurs_controller.rb +++ b/app/controllers/manager/administrateurs_controller.rb @@ -42,7 +42,7 @@ module Manager administrateur.dossiers.each(&:delete_and_keep_track) administrateur.destroy - logger.info("L'administrateur #{administrateur.id} est supprimé par #{current_user.id}") + logger.info("L'administrateur #{administrateur.id} est supprimé par #{current_administration.id}") flash[:notice] = "L'administrateur #{administrateur.id} est supprimé" redirect_to manager_administrateurs_path diff --git a/app/controllers/sessions/sessions_controller.rb b/app/controllers/sessions/sessions_controller.rb deleted file mode 100644 index 4bff2a161..000000000 --- a/app/controllers/sessions/sessions_controller.rb +++ /dev/null @@ -1,23 +0,0 @@ -class Sessions::SessionsController < Devise::SessionsController - before_action :before_sign_in, only: [:create] - - layout 'new_application' - - def before_sign_in - if user_signed_in? - sign_out :user - end - - if instructeur_signed_in? - sign_out :instructeur - end - - if administrateur_signed_in? - sign_out :administrateur - end - - if administration_signed_in? - sign_out :administration - end - end -end diff --git a/app/controllers/users/activate_controller.rb b/app/controllers/users/activate_controller.rb index c8553c3a4..657e2cf7c 100644 --- a/app/controllers/users/activate_controller.rb +++ b/app/controllers/users/activate_controller.rb @@ -14,36 +14,25 @@ class Users::ActivateController < ApplicationController end def create - password = create_user_params[:password] user = User.reset_password_by_token({ - password: password, - password_confirmation: password, - reset_password_token: create_user_params[:reset_password_token] + password: user_params[:password], + reset_password_token: user_params[:reset_password_token] }) - if user && user.errors.empty? + if user.valid? sign_in(user, scope: :user) flash.notice = "Mot de passe enregistré" redirect_to instructeur_procedures_path else flash.alert = user.errors.full_messages - redirect_to users_activate_path(token: create_user_params[:reset_password_token]) + redirect_to users_activate_path(token: user_params[:reset_password_token]) end end private - def create_user_params + def user_params params.require(:user).permit(:reset_password_token, :password) end - - def try_to_authenticate(klass, email, password) - resource = klass.find_for_database_authentication(email: email) - - if resource&.valid_password?(password) - sign_in resource - resource.force_sync_credentials - end - end end diff --git a/app/controllers/users/confirmations_controller.rb b/app/controllers/users/confirmations_controller.rb index ec5755987..b66d836be 100644 --- a/app/controllers/users/confirmations_controller.rb +++ b/app/controllers/users/confirmations_controller.rb @@ -42,7 +42,6 @@ class Users::ConfirmationsController < Devise::ConfirmationsController if sign_in_after_confirmation?(resource) resource.remember_me = true sign_in(resource) - resource.force_sync_credentials after_sign_in_path_for(resource_name) else super(resource_name, resource) diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb index e3b3f9536..13f751a42 100644 --- a/app/controllers/users/sessions_controller.rb +++ b/app/controllers/users/sessions_controller.rb @@ -1,4 +1,4 @@ -class Users::SessionsController < Sessions::SessionsController +class Users::SessionsController < Devise::SessionsController include ProcedureContextConcern include TrustedDeviceConcern include ActionView::Helpers::DateHelper @@ -7,33 +7,15 @@ class Users::SessionsController < Sessions::SessionsController before_action :restore_procedure_context, only: [:new, :create] - # GET /resource/sign_in - def new - @user = User.new - end - # POST /resource/sign_in def create - remember_me = params[:user][:remember_me] == '1' + user = User.find_by(email: params[:user][:email]) - if resource_locked?(try_to_authenticate(User, remember_me)) - flash.alert = 'Votre compte est verrouillé.' - new - return render :new, status: 401 + if user&.valid_password?(params[:user][:password]) + user.update(loged_in_with_france_connect: nil) end - if user_signed_in? - current_user.update(loged_in_with_france_connect: nil) - end - - if instructeur_signed_in? || user_signed_in? - set_flash_message :notice, :signed_in - redirect_to after_sign_in_path_for(:user) - else - flash.alert = 'Mauvais couple login / mot de passe' - new - render :new, status: 401 - end + super end def link_sent @@ -91,23 +73,4 @@ class Users::SessionsController < Sessions::SessionsController redirect_to link_sent_path(email: instructeur.email) end end - - private - - def try_to_authenticate(klass, remember_me = false) - resource = klass.find_for_database_authentication(email: params[:user][:email]) - - if resource.present? - if resource.valid_password?(params[:user][:password]) - resource.remember_me = remember_me - sign_in resource - resource.force_sync_credentials - end - end - resource - end - - def resource_locked?(resource) - resource.present? && resource.access_locked? - end end diff --git a/app/models/administrateur.rb b/app/models/administrateur.rb index 0a3007694..89fbbecc0 100644 --- a/app/models/administrateur.rb +++ b/app/models/administrateur.rb @@ -1,5 +1,4 @@ class Administrateur < ApplicationRecord - include CredentialsSyncableConcern include EmailSanitizableConcern include ActiveRecord::SecureToken @@ -9,7 +8,7 @@ class Administrateur < ApplicationRecord has_many :services has_many :dossiers, -> { state_not_brouillon }, through: :procedures - has_one :user + has_one :user, dependent: :nullify before_validation -> { sanitize_email(:email) } @@ -48,7 +47,7 @@ class Administrateur < ApplicationRecord def registration_state if active? 'Actif' - elsif reset_password_period_valid? + elsif user.reset_password_period_valid? 'En attente' else 'Expiré' @@ -56,7 +55,7 @@ class Administrateur < ApplicationRecord end def invitation_expired? - !active && !reset_password_period_valid? + !active && !user.reset_password_period_valid? end def self.reset_password(reset_password_token, password) diff --git a/app/models/administration.rb b/app/models/administration.rb index 7073779d7..1e5958ef9 100644 --- a/app/models/administration.rb +++ b/app/models/administration.rb @@ -8,32 +8,11 @@ class Administration < ApplicationRecord end def invite_admin(email) - password = SecureRandom.hex + user = User.create_or_promote_to_administrateur(email, SecureRandom.hex) - user = User.find_by(email: email) - - if user.nil? - # set confirmed_at otherwise admin confirmation doesnt work - # we somehow mess up using reset_password logic instead of - # confirmation_logic - # FIXME - user = User.create( - email: email, - password: password, - confirmed_at: Time.zone.now - ) - end - - if user.errors.empty? - if user.instructeur.nil? - Instructeur.create!(email: email, user: user) - end - - if user.administrateur.nil? - administrateur = Administrateur.create!(email: email, active: false, user: user) - AdministrationMailer.new_admin_email(administrateur, self).deliver_later - user.invite_administrateur!(id) - end + if user.valid? + AdministrationMailer.new_admin_email(user.administrateur, self).deliver_later + user.invite_administrateur!(id) end user diff --git a/app/models/commentaire.rb b/app/models/commentaire.rb index 376296064..4ff31f768 100644 --- a/app/models/commentaire.rb +++ b/app/models/commentaire.rb @@ -4,7 +4,6 @@ class Commentaire < ApplicationRecord belongs_to :user belongs_to :instructeur - mount_uploader :file, CommentaireFileUploader validate :messagerie_available?, on: :create has_one_attached :piece_jointe @@ -48,15 +47,8 @@ class Commentaire < ApplicationRecord end def file_url - if piece_jointe.attached? - if piece_jointe.virus_scanner.safe? - Rails.application.routes.url_helpers.url_for(piece_jointe) - end - elsif Rails.application.secrets.fog[:enabled] - RemoteDownloader.new(file.path).url - elsif file&.url - # FIXME: this is horrible but used only in dev and will be removed after migration - File.join(LOCAL_DOWNLOAD_URL, file.url) + if piece_jointe.attached? && piece_jointe.virus_scanner.safe? + Rails.application.routes.url_helpers.url_for(piece_jointe) end end diff --git a/app/models/concerns/credentials_syncable_concern.rb b/app/models/concerns/credentials_syncable_concern.rb deleted file mode 100644 index f42b4adae..000000000 --- a/app/models/concerns/credentials_syncable_concern.rb +++ /dev/null @@ -1,18 +0,0 @@ -module CredentialsSyncableConcern - extend ActiveSupport::Concern - - included do - after_update :sync_credentials - end - - def sync_credentials - if saved_change_to_email? || saved_change_to_encrypted_password? - return force_sync_credentials - end - true - end - - def force_sync_credentials - SyncCredentialsService.new(self.class, email_before_last_save, email, encrypted_password).change_credentials! - end -end diff --git a/app/models/instructeur.rb b/app/models/instructeur.rb index b61fd3b2d..5fcb1b205 100644 --- a/app/models/instructeur.rb +++ b/app/models/instructeur.rb @@ -1,5 +1,4 @@ class Instructeur < ApplicationRecord - include CredentialsSyncableConcern include EmailSanitizableConcern has_and_belongs_to_many :administrateurs diff --git a/app/models/user.rb b/app/models/user.rb index 42f7ceaa1..8a6fdd86e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,5 +1,4 @@ class User < ApplicationRecord - include CredentialsSyncableConcern include EmailSanitizableConcern enum loged_in_with_france_connect: { @@ -62,6 +61,32 @@ class User < ApplicationRecord AdministrateurMailer.activate_before_expiration(self, reset_password_token).deliver_later end + def self.create_or_promote_to_instructeur(email, password, administrateurs: []) + user = User + .create_with(password: password, confirmed_at: Time.zone.now) + .find_or_create_by(email: email) + + if user.valid? + if user.instructeur_id.nil? + user.create_instructeur!(email: email) + end + + user.instructeur.administrateurs << administrateurs + end + + user + end + + def self.create_or_promote_to_administrateur(email, password) + user = User.create_or_promote_to_instructeur(email, password) + + if user.valid? && user.administrateur_id.nil? + user.create_administrateur!(email: email) + end + + user + end + private def link_invites! diff --git a/app/services/ip_service.rb b/app/services/ip_service.rb index 6ca7fdc5e..2f69c55d2 100644 --- a/app/services/ip_service.rb +++ b/app/services/ip_service.rb @@ -3,13 +3,7 @@ class IPService def ip_trusted?(ip) ip_address = parse_address(ip) - if ip_address.nil? - false - elsif trusted_networks.present? - trusted_networks.any? { |network| network.include?(ip_address) } - else - false - end + trusted_networks.any? { |network| network.include?(ip_address) } end private diff --git a/app/services/sync_credentials_service.rb b/app/services/sync_credentials_service.rb deleted file mode 100644 index 13322b1f4..000000000 --- a/app/services/sync_credentials_service.rb +++ /dev/null @@ -1,33 +0,0 @@ -class SyncCredentialsService - def initialize(klass, email_before_last_save, email, encrypted_password) - @klass = klass - @email_before_last_save = email_before_last_save - @email = email - @encrypted_password = encrypted_password - end - - def change_credentials! - if @klass != User - user = User.find_by(email: @email_before_last_save) - if user && !user.update_columns(email: @email, encrypted_password: @encrypted_password) - return false - end - end - - if @klass != Instructeur - instructeur = Instructeur.find_by(email: @email_before_last_save) - if instructeur && !instructeur.update_columns(email: @email, encrypted_password: @encrypted_password) - return false - end - end - - if @klass != Administrateur - administrateur = Administrateur.find_by(email: @email_before_last_save) - if administrateur && !administrateur.update_columns(email: @email, encrypted_password: @encrypted_password) - return false - end - end - - true - end -end diff --git a/app/uploaders/commentaire_file_uploader.rb b/app/uploaders/commentaire_file_uploader.rb deleted file mode 100644 index 93131fb94..000000000 --- a/app/uploaders/commentaire_file_uploader.rb +++ /dev/null @@ -1,23 +0,0 @@ -class CommentaireFileUploader < BaseUploader - def root - Rails.root.join("public") - end - - if Rails.application.secrets.fog[:enabled] - storage :fog - else - storage :file - end - - def store_dir - "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" - end - - def extension_whitelist - ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'odt', 'ods', 'odp', 'jpg', 'jpeg', 'png', 'zip', 'txt'] - end - - def accept_extension_list - extension_whitelist.map { |e| ".#{e}" }.join(",") - end -end diff --git a/app/views/administrateurs/sessions/new.html.haml b/app/views/administrateurs/sessions/new.html.haml deleted file mode 100644 index c24523417..000000000 --- a/app/views/administrateurs/sessions/new.html.haml +++ /dev/null @@ -1,26 +0,0 @@ -#form-login - %h2#login_admin - = t('dynamics.admin.connexion_title') - - %br - %br - #new-user - = form_for @administrateur, url: { controller: 'administrateurs/sessions', action: :create } do |f| - %h4 - = f.label :email - .input-group - .input-group-addon - %span.fa.fa-user - = f.email_field :email, class: 'form-control' - %br - %h4 - = f.label 'Mot de passe' - .input-group - .input-group-addon - %span.fa.fa-asterisk - = f.password_field :password, class: 'form-control', value: @administrateur.password - %br - %br - .actions - = f.submit "Se connecter", class: 'btn btn-primary' - %br diff --git a/app/views/avis_mailer/avis_invitation.html.haml b/app/views/avis_mailer/avis_invitation.html.haml index 1f36944f8..bd08f19ac 100644 --- a/app/views/avis_mailer/avis_invitation.html.haml +++ b/app/views/avis_mailer/avis_invitation.html.haml @@ -25,7 +25,7 @@ - if @avis.instructeur.present? %p - = link_to "Connectez-vous pour donner votre avis", avis_link + = link_to "Cliquez ici pour donner votre avis", avis_link - else %p = link_to "Inscrivez-vous pour donner votre avis", avis_link diff --git a/app/views/instructeurs/sessions/new.html.haml b/app/views/instructeurs/sessions/new.html.haml deleted file mode 100644 index ada6dabbb..000000000 --- a/app/views/instructeurs/sessions/new.html.haml +++ /dev/null @@ -1,26 +0,0 @@ -#form-login - %h2#instructeur_login Instructeur - - %br - %br - #new-user - = form_for @instructeur, url: instructeur_session_path, method: :post do |f| - %h4 - = f.label :email - .input-group - .input-group-addon - %span.fa.fa-user - = f.email_field :email, class: 'form-control' - %br - %h4 - = f.label :password - .input-group - .input-group-addon - %span.fa.fa-asterisk - = f.password_field :password, autocomplete: "off", class: 'form-control', value: @instructeur.password - %br - %br - .actions - = f.submit "Se connecter", class: 'btn btn-primary' - %br - = render "instructeurs/shared/links" diff --git a/app/views/stats/index.html.haml b/app/views/stats/index.html.haml index 9d8821a07..9a3f1a5c1 100644 --- a/app/views/stats/index.html.haml +++ b/app/views/stats/index.html.haml @@ -34,7 +34,7 @@ .stat-card.stat-card-half.pull-left %span.stat-card-title - Pourcentage de contact usagers + Pourcentage de contact usager .chart-container .chart diff --git a/app/views/users/sessions/new.html.haml b/app/views/users/sessions/new.html.haml index eb88d230a..9f7a9962f 100644 --- a/app/views/users/sessions/new.html.haml +++ b/app/views/users/sessions/new.html.haml @@ -1,32 +1,29 @@ = content_for(:page_id, 'auth') .auth-form.sign-in-form - - if resource_name == :user - %p.register - %span - Nouveau sur demarches‑simplifiees.fr ? - = link_to "Créer un compte", new_registration_path(resource_name), class: "button primary auth-signup-button" + %p.register + %span + Nouveau sur demarches‑simplifiees.fr ? + = link_to "Créer un compte", new_user_registration_path, class: "button primary auth-signup-button" - %hr + %hr - = form_for @user, url: user_session_path, html: { class: "form" } do |f| + = form_for User.new, url: user_session_path, html: { class: "form" } do |f| %h1 Connectez-vous = f.label :email, "Email" = f.text_field :email, autofocus: true = f.label :password, "Mot de passe" - = f.password_field :password, value: @user.password, placeholder: "8 caractères minimum" + = f.password_field :password, placeholder: "8 caractères minimum" .auth-options - - if devise_mapping.rememberable? - %div - = f.check_box :remember_me, as: :boolean - = f.label :remember_me, "Se souvenir de moi", class: 'remember-me' + %div + = f.check_box :remember_me, as: :boolean + = f.label :remember_me, "Se souvenir de moi", class: 'remember-me' - - if [:user, :instructeur].include?(resource_name) - .text-right - = link_to "Mot de passe oublié ?", new_password_path(resource_name), class: "link" + .text-right + = link_to "Mot de passe oublié ?", new_user_password_path, class: "link" = f.submit "Se connecter", class: "button large primary expand" diff --git a/config/application.rb b/config/application.rb index be464bbf2..5f6cbc055 100644 --- a/config/application.rb +++ b/config/application.rb @@ -41,5 +41,6 @@ module TPS end config.ds_weekly_overview = ENV['APP_NAME'] == 'tps' + config.middleware.use Rack::Attack end end diff --git a/config/initializers/rack_attack.rb b/config/initializers/rack_attack.rb new file mode 100644 index 000000000..9a18f8936 --- /dev/null +++ b/config/initializers/rack_attack.rb @@ -0,0 +1,27 @@ +class Rack::Attack + throttle('/users/sign_in/ip', limit: 5, period: 20.seconds) do |req| + if req.path == '/users/sign_in' && req.post? && rack_attack_enabled? + req.remote_ip + end + end + + throttle('stats/ip', limit: 5, period: 20.seconds) do |req| + if req.path == '/stats' && rack_attack_enabled? + req.remote_ip + end + end + + throttle('contact/ip', limit: 5, period: 20.seconds) do |req| + if req.path == '/contact' && req.post? && rack_attack_enabled? + req.remote_ip + end + end + + Rack::Attack.safelist('allow from localhost') do |req| + IPService.ip_trusted?(req.remote_ip) + end + + def self.rack_attack_enabled? + ENV['RACK_ATTACK_ENABLE'] == 'true' + end +end diff --git a/config/initializers/rack_attack_request.rb b/config/initializers/rack_attack_request.rb new file mode 100644 index 000000000..fa72e9844 --- /dev/null +++ b/config/initializers/rack_attack_request.rb @@ -0,0 +1,7 @@ +class Rack::Attack + class Request < ::Rack::Request + def remote_ip + @remote_ip ||= (env['action_dispatch.remote_ip'] || ip).to_s + end + end +end diff --git a/lib/tasks/deployment/20190226101524_add_procedure_administrateur_to_administrateurs.rake b/lib/tasks/deployment/20190226101524_add_procedure_administrateur_to_administrateurs.rake deleted file mode 100644 index 6843a58e2..000000000 --- a/lib/tasks/deployment/20190226101524_add_procedure_administrateur_to_administrateurs.rake +++ /dev/null @@ -1,18 +0,0 @@ -namespace :after_party do - desc 'Deployment task: add_procedure_administrateur_to_administrateurs' - task add_procedure_administrateur_to_administrateurs: :environment do - rake_puts "Running deploy task: 'add_procedure_administrateur_to_administrateurs'" - procedures = Procedure.includes(:administrateurs) - progress = ProgressReport.new(procedures.count) - - procedures.find_each do |procedure| - if !procedure.administrateurs.include?(procedure.administrateur) - procedure.administrateurs << procedure.administrateur - end - progress.inc - end - - progress.finish - AfterParty::TaskRecord.create version: '20190226101524' - end -end diff --git a/lib/tasks/deployment/20190425102459_migrate_virus_scans.rake b/lib/tasks/deployment/20190425102459_migrate_virus_scans.rake deleted file mode 100644 index e72d6c1ee..000000000 --- a/lib/tasks/deployment/20190425102459_migrate_virus_scans.rake +++ /dev/null @@ -1,22 +0,0 @@ -namespace :after_party do - desc 'Deployment task: migrate_virus_scans' - task migrate_virus_scans: :environment do - puts "Running deploy task 'migrate_virus_scans'" - - virus_scans = VirusScan.all - progress = ProgressReport.new(virus_scans.count) - virus_scans.find_each do |virus_scan| - blob = ActiveStorage::Blob.find_by(key: virus_scan.blob_key) - if blob - metadata = { virus_scan_result: virus_scan.status, scanned_at: virus_scan.scanned_at } - blob.update_column(:metadata, blob.metadata.merge(metadata)) - end - progress.inc - end - progress.finish - - # Update task as completed. If you remove the line below, the task will - # run with every deploy (or every time you call after_party:run). - AfterParty::TaskRecord.create version: '20190425102459' - end -end diff --git a/lib/tasks/deployment/20190429103024_add_procedure_administrateur_to_administrateurs_for_hidden_procedures.rake b/lib/tasks/deployment/20190429103024_add_procedure_administrateur_to_administrateurs_for_hidden_procedures.rake deleted file mode 100644 index 182387bf4..000000000 --- a/lib/tasks/deployment/20190429103024_add_procedure_administrateur_to_administrateurs_for_hidden_procedures.rake +++ /dev/null @@ -1,19 +0,0 @@ -namespace :after_party do - desc 'Deployment task: add_procedure_administrateur_to_administrateurs_for_hidden_procedures' - task add_procedure_administrateur_to_administrateurs_for_hidden_procedures: :environment do - rake_puts "Running deploy task: 'add_procedure_administrateur_to_administrateurs_for_hidden_procedures'" - hidden_procedures = Procedure.unscoped.hidden.includes(:administrateurs) - progress = ProgressReport.new(hidden_procedures.count) - - hidden_procedures.find_each do |procedure| - deprecated_administrateur = Administrateur.find_by(id: procedure.administrateur_id) - if deprecated_administrateur && !procedure.administrateurs.include?(deprecated_administrateur) - procedure.administrateurs << deprecated_administrateur - end - progress.inc - end - - progress.finish - AfterParty::TaskRecord.create version: '20190429103024' - end -end diff --git a/lib/tasks/deployment/20190627142239_enable_secured_login_for_all.rake b/lib/tasks/deployment/20190627142239_enable_secured_login_for_all.rake deleted file mode 100644 index 70120bb96..000000000 --- a/lib/tasks/deployment/20190627142239_enable_secured_login_for_all.rake +++ /dev/null @@ -1,8 +0,0 @@ -namespace :after_party do - desc 'Deployment task: enable_secured_login_for_all' - task enable_secured_login_for_all: :environment do - Gestionnaire.update_all(features: { "enable_email_login_token": true }) - - AfterParty::TaskRecord.create version: '20190627142239' - end -end diff --git a/lib/tasks/deployment/20190701131030_purge_unattached_piece_justificative.rake b/lib/tasks/deployment/20190701131030_purge_unattached_piece_justificative.rake deleted file mode 100644 index 5078b5142..000000000 --- a/lib/tasks/deployment/20190701131030_purge_unattached_piece_justificative.rake +++ /dev/null @@ -1,21 +0,0 @@ -namespace :after_party do - desc 'Deployment task: purge_unattached_piece_justificative' - task purge_unattached_piece_justificative: :environment do - puts "Running deploy task 'purge_unattached_piece_justificative'" - - piece_justificatives = PieceJustificative.where(type_de_piece_justificative_id: nil) - progress = ProgressReport.new(piece_justificatives.count) - piece_justificatives.find_each do |pj| - # detach from dossier to ensure we do not trigger touch - pj.update_column(:dossier_id, nil) - pj.remove_content! - pj.destroy - progress.inc - end - progress.finish - - # Update task as completed. If you remove the line below, the task will - # run with every deploy (or every time you call after_party:run). - AfterParty::TaskRecord.create version: '20190701131030' - end -end diff --git a/lib/tasks/deployment/20190819100424_clean_procedure_presentation_from_followers_gestionnaires.rake b/lib/tasks/deployment/20190819100424_clean_procedure_presentation_from_followers_gestionnaires.rake new file mode 100644 index 000000000..33a1997e0 --- /dev/null +++ b/lib/tasks/deployment/20190819100424_clean_procedure_presentation_from_followers_gestionnaires.rake @@ -0,0 +1,30 @@ +namespace :after_party do + desc 'Deployment task: clean_procedure_presentation_from_followers_gestionnaires' + task clean_procedure_presentation_from_followers_gestionnaires: :environment do + ProcedurePresentation.find_each do |pp| + if pp.sort["table"] == "followers_gestionnaires" + pp.sort["table"] = "followers_instructeurs" + end + + pp.displayed_fields.each do |df| + if df["table"] == "followers_gestionnaires" + df["table"] = "followers_instructeurs" + end + end + + pp.filters.each do |(_name, values)| + values.each do |value| + if value["table"] == "followers_gestionnaires" + value["table"] = "followers_instructeurs" + end + end + end + + begin + pp.save! + rescue ActiveRecord::RecordInvalid + end + end + AfterParty::TaskRecord.create version: '20190819100424' + end +end diff --git a/spec/controllers/manager/administrateurs_controller_spec.rb b/spec/controllers/manager/administrateurs_controller_spec.rb index f7b9a11e9..0d954ee3c 100644 --- a/spec/controllers/manager/administrateurs_controller_spec.rb +++ b/spec/controllers/manager/administrateurs_controller_spec.rb @@ -31,4 +31,18 @@ describe Manager::AdministrateursController, type: :controller do it { expect { subject }.to change(Administrateur, :count).by(0) } end end + + describe '#delete' do + let!(:admin) { create(:administrateur) } + + before { sign_in administration } + + subject { delete :delete, params: { id: admin.id } } + + it 'deletes the admin' do + subject + + expect(Administrateur.find_by(id: admin.id)).to be_nil + end + end end diff --git a/spec/controllers/sessions/sessions_controller_spec.rb b/spec/controllers/sessions/sessions_controller_spec.rb deleted file mode 100644 index 06b4ec51c..000000000 --- a/spec/controllers/sessions/sessions_controller_spec.rb +++ /dev/null @@ -1,35 +0,0 @@ -require 'spec_helper' - -describe Sessions::SessionsController, type: :controller do - controller Sessions::SessionsController do - def create - render json: '' - end - end - - let(:user) { create(:user) } - - describe '#create' do - before do - @request.env["devise.mapping"] = Devise.mappings[:user] - end - - it 'calls before_sign_in' do - expect_any_instance_of(Sessions::SessionsController).to receive(:before_sign_in) - post :create - end - end - - describe '#create with user connected' do - before do - @request.env["devise.mapping"] = Devise.mappings[:user] - - allow_any_instance_of(described_class).to receive(:user_signed_in?).and_return(true) - end - - it 'calls sign out for user' do - expect_any_instance_of(described_class).to receive(:sign_out).with(:user) - post :create - end - end -end diff --git a/spec/controllers/users/activate_controller_spec.rb b/spec/controllers/users/activate_controller_spec.rb index 917b560be..01aeda490 100644 --- a/spec/controllers/users/activate_controller_spec.rb +++ b/spec/controllers/users/activate_controller_spec.rb @@ -17,4 +17,24 @@ describe Users::ActivateController, type: :controller do it { expect(controller).not_to have_received(:trust_device) } end end + + describe '#create' do + let!(:user) { create(:user) } + let(:token) { user.send(:set_reset_password_token) } + let(:password) { 'another-password-ok?' } + + before { post :create, params: { user: { reset_password_token: token, password: password } } } + + context 'when the token is ok' do + it { expect(user.reload.valid_password?(password)).to be true } + it { expect(response).to redirect_to(instructeur_procedures_path) } + end + + context 'when the token is bad' do + let(:token) { 'bad' } + + it { expect(user.reload.valid_password?(password)).to be false } + it { expect(response).to redirect_to(users_activate_path(token: token)) } + end + end end diff --git a/spec/controllers/users/sessions_controller_spec.rb b/spec/controllers/users/sessions_controller_spec.rb index 0a42fc26a..6f09382fa 100644 --- a/spec/controllers/users/sessions_controller_spec.rb +++ b/spec/controllers/users/sessions_controller_spec.rb @@ -9,71 +9,74 @@ describe Users::SessionsController, type: :controller do end describe '#create' do - context "when the user is also a instructeur and an administrateur" do - let!(:administrateur) { create(:administrateur, email: email, password: password) } - let(:instructeur) { administrateur.instructeur } - let(:user) { instructeur.user } - let(:trusted_device) { true } - let(:send_password) { password } + let(:user) { create(:user, email: email, password: password, loged_in_with_france_connect: 'particulier') } + let(:send_password) { password } + let(:remember_me) { '0' } - before do - allow(controller).to receive(:trusted_device?).and_return(trusted_device) - allow(InstructeurMailer).to receive(:send_login_token).and_return(double(deliver_later: true)) + subject do + post :create, params: { + user: { + email: email, + password: send_password, + remember_me: remember_me + } + } + end + + context 'when the credentials are right' do + it 'signs in' do + subject + + expect(response).to redirect_to(root_path) + expect(controller.current_user).to eq(user) + expect(user.reload.loged_in_with_france_connect).to be(nil) + expect(user.reload.remember_created_at).to be_nil end - subject do - post :create, params: { user: { email: email, password: send_password } } - user.reload - end + context 'when remember_me is specified' do + let(:remember_me) { '1' } - context 'when the device is not trusted' do - before do - Flipflop::FeatureSet.current.test!.switch!(:bypass_email_login_token, false) - end - let(:trusted_device) { false } - - it 'redirects to the send_linked_path' do + it 'remembers' do subject - expect(controller).to redirect_to(link_sent_path(email: user.email)) - - expect(controller.current_user).to eq(user) - expect(controller.current_instructeur).to eq(instructeur) - # WTF? - # expect(controller.current_administrateur).to eq(administrateur) - expect(user.loged_in_with_france_connect).to eq(nil) + expect(user.reload.remember_created_at).to be_present end end - context 'when the device is trusted' do - it 'signs in as user, instructeur and adminstrateur' do + context 'when a previous path was registered' do + let(:stored_path) { 'a_path' } + + before { controller.store_location_for(:user, stored_path) } + + it 'redirects to that previous path' do subject - expect(response.redirect?).to be(true) - expect(controller).not_to redirect_to link_sent_path(email: email) - # TODO when signing in as non-administrateur, and not starting a demarche, log in to instructeur path - # expect(controller).to redirect_to instructeur_procedures_path - - expect(controller.current_user).to eq(user) - expect(controller.current_instructeur).to eq(instructeur) - expect(controller.current_administrateur).to eq(administrateur) - expect(user.loged_in_with_france_connect).to be(nil) + expect(response).to redirect_to(stored_path) end end - context 'when the credentials are wrong' do - let(:send_password) { 'wrong_password' } + context 'when the user is locked' do + before { user.lock_access! } - it 'fails to sign in with bad credentials' do + it 'redirects to new_path' do subject - expect(response.unauthorized?).to be(true) - expect(controller.current_user).to be(nil) - expect(controller.current_instructeur).to be(nil) - expect(controller.current_administrateur).to be(nil) + expect(response).to render_template(:new) + expect(flash.alert).to eq(I18n.t('devise.failure.invalid')) end end end + + context 'when the credentials are wrong' do + let(:send_password) { 'wrong_password' } + + it 'fails to sign in with bad credentials' do + subject + + expect(response).to render_template(:new) + expect(controller.current_user).to be(nil) + end + end end describe '#destroy' do diff --git a/spec/factories/administrateur.rb b/spec/factories/administrateur.rb index 5ab633aea..277cfd415 100644 --- a/spec/factories/administrateur.rb +++ b/spec/factories/administrateur.rb @@ -4,18 +4,11 @@ FactoryBot.define do email { generate(:administrateur_email) } transient do - user { nil } password { 'mon chien aime les bananes' } end - after(:create) do |administrateur, evaluator| - if evaluator.user.present? - user = evaluator.user - else - user = create(:user, email: administrateur.email, password: evaluator.password, administrateur: administrateur) - end - - create(:instructeur, email: administrateur.email, user: user) + initialize_with do + User.create_or_promote_to_administrateur(email, password).administrateur end end diff --git a/spec/factories/instructeur.rb b/spec/factories/instructeur.rb index 92d33695b..eb51ed6fa 100644 --- a/spec/factories/instructeur.rb +++ b/spec/factories/instructeur.rb @@ -8,14 +8,8 @@ FactoryBot.define do password { 'somethingverycomplated!' } end - after(:create) do |instructeur, evaluator| - if evaluator.user.present? - user = evaluator.user - else - user = create(:user, email: instructeur.email, password: evaluator.password) - end - - instructeur.update!(user: user) + initialize_with do + User.create_or_promote_to_instructeur(email, password).instructeur end end end diff --git a/spec/features/sessions/sign_in_spec.rb b/spec/features/sessions/sign_in_spec.rb index 8c549c23f..958c3b7c2 100644 --- a/spec/features/sessions/sign_in_spec.rb +++ b/spec/features/sessions/sign_in_spec.rb @@ -13,6 +13,17 @@ feature 'Signin in:' do expect(page).to have_current_path dossiers_path end + scenario 'an existing user can lock its account' do + visit root_path + click_on 'Connexion' + + 5.times { sign_in_with user.email, 'bad password' } + expect(user.reload.access_locked?).to be false + + sign_in_with user.email, 'bad password' + expect(user.reload.access_locked?).to be true + end + context 'when visiting a procedure' do let(:procedure) { create :simple_procedure, :with_service } diff --git a/spec/lib/tasks/2019_05_29_migrate_commentaire_pj_spec.rb b/spec/lib/tasks/2019_05_29_migrate_commentaire_pj_spec.rb deleted file mode 100644 index b05875879..000000000 --- a/spec/lib/tasks/2019_05_29_migrate_commentaire_pj_spec.rb +++ /dev/null @@ -1,62 +0,0 @@ -describe '2019_05_29_migrate_commentaire_pj.rake' do - let(:rake_task) { Rake::Task['2019_05_29_migrate_commentaire_pj:run'] } - - let(:commentaires) do - [ - create(:commentaire), - create(:commentaire, :with_file), - create(:commentaire, :with_file) - ] - end - - before do - commentaires.each do |commentaire| - if commentaire.file.present? - stub_request(:get, commentaire.file_url) - .to_return(status: 200, body: File.read(commentaire.file.path)) - end - end - end - - after do - ENV['LIMIT'] = nil - rake_task.reenable - end - - it 'should migrate pj' do - comment_updated_at = Commentaire.last.updated_at - dossier_updated_at = Commentaire.last.dossier.updated_at - expect(Commentaire.all.map(&:piece_jointe).map(&:attached?)).to eq([false, false, false]) - rake_task.invoke - expect(Commentaire.where(file: nil).count).to eq(1) - expect(Commentaire.all.map(&:piece_jointe).map(&:attached?)).to eq([false, true, true]) - expect(Commentaire.last.updated_at).to eq(comment_updated_at) - expect(Commentaire.last.dossier.updated_at).to eq(dossier_updated_at) - end - - it 'should migrate pj within limit' do - expect(Commentaire.all.map(&:piece_jointe).map(&:attached?)).to eq([false, false, false]) - ENV['LIMIT'] = '1' - rake_task.invoke - expect(Commentaire.where(file: nil).count).to eq(1) - expect(Commentaire.all.map(&:piece_jointe).map(&:attached?)).to eq([false, true, false]) - end - - context 'when a commentaire’s dossier is hidden' do - let(:hidden_dossier) { create(:dossier, :en_construction, :hidden) } - let(:commentaire) { create(:commentaire, :with_file, dossier: hidden_dossier) } - let(:commentaires) { [commentaire] } - - it 'should migrate the pj' do - comment_updated_at = commentaire.reload.updated_at - dossier_updated_at = hidden_dossier.reload.updated_at - - rake_task.invoke - commentaires.each(&:reload) - - expect(commentaire.piece_jointe.attached?).to be true - expect(commentaire.updated_at).to eq(comment_updated_at) - expect(hidden_dossier.updated_at).to eq(dossier_updated_at) - end - end -end diff --git a/spec/lib/tasks/deployment/20190819100424_clean_procedure_presentation_from_followers_gestionnaires_spec.rb b/spec/lib/tasks/deployment/20190819100424_clean_procedure_presentation_from_followers_gestionnaires_spec.rb new file mode 100644 index 000000000..70f8f27fc --- /dev/null +++ b/spec/lib/tasks/deployment/20190819100424_clean_procedure_presentation_from_followers_gestionnaires_spec.rb @@ -0,0 +1,84 @@ +describe '20190819100424_clean_procedure_presentation_from_followers_gestionnaires.rake' do + let(:rake_task) { Rake::Task['after_party:clean_procedure_presentation_from_followers_gestionnaires'] } + + let(:procedure) { create(:procedure, :with_type_de_champ, :with_type_de_champ_private) } + let(:assign_to) { create(:assign_to, procedure: procedure) } + + let!(:procedure_presentation) do + pp = ProcedurePresentation.new( + assign_to: assign_to, + sort: { + "order" => "asc", + "table" => "followers_gestionnaires", + "column" => "email" + }, + filters: { + "tous" => [], + "suivis" => [], + "traites" => [{ + "label" => "Email instructeur", + "table" => "followers_gestionnaires", + "value" => "mail@simon.lehericey.net", + "column" => "email" + } + ], + "a-suivre" => [], + "archives" => [] + }, + displayed_fields: [ + { + "column" => "email", + "label" => "Demandeur", + "table" => "user" + }, + { + "column" => "email", + "label" => "Email instructeur", + "table" => "followers_gestionnaires" + } + ] + ) + pp.save(validate: false) + pp + end + + before do + rake_task.invoke + procedure_presentation.reload + end + + after { rake_task.reenable } + + it do + expect(procedure_presentation.displayed_fields[1]["table"]).to eq("followers_instructeurs") + expect(procedure_presentation.sort["table"]).to eq("followers_instructeurs") + expect(procedure_presentation.filters["traites"][0]["table"]).to eq("followers_instructeurs") + end + + context 'with an invalid procedure_presentation' do + let!(:procedure_presentation) do + pp = ProcedurePresentation.new( + assign_to: assign_to, + filters: { + "tous" => [], + "suivis" => [], + "traites" => [{ + "label" => "Email instructeur", + "table" => "invalid table", + "value" => "mail@simon.lehericey.net", + "column" => "email" + } + ], + "a-suivre" => [], + "archives" => [] + }, + displayed_fields: [] + ) + pp.save(validate: false) + pp + end + + it 'does not stop the script' do + end + end +end diff --git a/spec/middlewares/rack_attack_spec.rb b/spec/middlewares/rack_attack_spec.rb new file mode 100644 index 000000000..1ec71cd96 --- /dev/null +++ b/spec/middlewares/rack_attack_spec.rb @@ -0,0 +1,56 @@ +require "rails_helper" + +describe Rack::Attack, type: :request do + let(:limit) { 5 } + let(:period) { 20 } + let(:ip) { "1.2.3.4" } + + before(:each) do + ENV['RACK_ATTACK_ENABLE'] = 'true' + setup_rack_attack_cache_store + avoid_test_overlaps_in_cache + end + + after do + ENV['RACK_ATTACK_ENABLE'] = 'false' + end + + def setup_rack_attack_cache_store + Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new + end + + def avoid_test_overlaps_in_cache + Rails.cache.clear + end + + context '/users/sign_in' do + before do + limit.times do + Rack::Attack.cache.count("/users/sign_in/ip:#{ip}", period) + end + end + + subject do + post "/users/sign_in", headers: { 'X-Forwarded-For': ip } + end + + it "throttle excessive requests by IP address" do + subject + + expect(response).to have_http_status(:too_many_requests) + end + + context 'when the ip is whitelisted' do + before do + allow(IPService).to receive(:ip_trusted?).and_return(true) + allow_any_instance_of(Users::SessionsController).to receive(:create).and_return(:ok) + end + + it "respects the whitelist" do + subject + + expect(response).not_to have_http_status(:too_many_requests) + end + end + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 1b817db8a..1f9f8eff6 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -100,4 +100,68 @@ describe User, type: :model do it { is_expected.to be_falsey } end end + + describe '.create_or_promote_to_instructeur' do + let(:email) { 'inst1@gmail.com' } + let(:password) { 'un super password !' } + let(:admins) { [] } + + subject { User.create_or_promote_to_instructeur(email, password, administrateurs: admins) } + + context 'without an existing user' do + it do + user = subject + expect(user.valid_password?(password)).to be true + expect(user.confirmed_at).to be_present + expect(user.instructeur).to be_present + end + + context 'with an administrateur' do + let(:admins) { [create(:administrateur)] } + + it do + user = subject + expect(user.instructeur.administrateurs).to eq(admins) + end + end + end + + context 'with an existing user' do + before { create(:user, email: email, password: 'démarches-simplifiées-pwd') } + + it 'keeps the previous password' do + user = subject + expect(user.valid_password?('démarches-simplifiées-pwd')).to be true + expect(user.instructeur).to be_present + end + + context 'with an existing instructeur' do + let(:old_admins) { [create(:administrateur)] } + let(:admins) { [create(:administrateur)] } + let!(:instructeur) { Instructeur.create(email: 'i@mail.com', administrateurs: old_admins) } + + before do + User + .find_by(email: email) + .update!(instructeur: instructeur) + end + + it 'keeps the existing instructeurs and adds administrateur' do + user = subject + expect(user.instructeur).to eq(instructeur) + expect(user.instructeur.administrateurs).to eq(old_admins + admins) + end + end + end + + context 'with an invalid email' do + let(:email) { 'invalid' } + + it 'does not build an instructeur' do + user = subject + expect(user.valid?).to be false + expect(user.instructeur).to be_nil + end + end + end end diff --git a/spec/services/ip_service_spec.rb b/spec/services/ip_service_spec.rb index c9c1d1641..2d2798fa0 100644 --- a/spec/services/ip_service_spec.rb +++ b/spec/services/ip_service_spec.rb @@ -28,6 +28,18 @@ describe IPService do it { is_expected.to be(false) } end + + context 'when the trusted network is not defined' do + it { is_expected.to be(false) } + end + + context 'when the trusted network is malformed' do + before do + ENV['TRUSTED_NETWORKS'] = 'bad network' + end + + it { is_expected.to be(false) } + end end context 'when a trusted network is defined' do