Merge pull request #5721 from betagouv/dev

2020-11-05-01
This commit is contained in:
krichtof 2020-11-05 16:40:10 +01:00 committed by GitHub
commit 42ddaf05f8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
56 changed files with 430 additions and 315 deletions

View file

@ -23,6 +23,7 @@ gem 'delayed_job_web'
gem 'devise' # Gestion des comptes utilisateurs
gem 'devise-async'
gem 'devise-i18n'
gem 'devise-two-factor', github: 'bryanfagan/devise-two-factor'
gem 'discard'
gem 'dotenv-rails', require: 'dotenv/rails-now' # dotenv should always be loaded before rails
gem 'ffi-geos', require: false
@ -49,8 +50,6 @@ gem 'kaminari', '1.2.1' # Pagination
gem 'lograge'
gem 'logstash-event'
gem 'mailjet'
gem 'omniauth-github'
gem 'omniauth-rails_csrf_protection', '~> 0.1'
gem 'openid_connect'
gem 'pg'
gem 'phonelib'
@ -65,6 +64,7 @@ gem 'rails-i18n' # Locales par défaut
gem 'rake-progressbar', require: false
gem 'react-rails'
gem 'rgeo-geojson'
gem 'rqrcode'
gem 'sanitize-url'
gem 'sassc-rails' # Use SCSS for stylesheets
gem 'sentry-raven'

View file

@ -1,3 +1,14 @@
GIT
remote: https://github.com/bryanfagan/devise-two-factor.git
revision: 60038a699b1847266f6ce0a3457fdc2cd24715be
specs:
devise-two-factor (3.1.1)
activesupport (< 6.1)
attr_encrypted (>= 1.3, < 4, != 2)
devise (~> 4.0)
railties (< 6.1)
rotp (~> 4.0)
GIT
remote: https://github.com/mina-deploy/mina.git
revision: 84fa84c7f7f94f9518ef9b7099396ab6676b5881
@ -101,6 +112,8 @@ GEM
activerecord (>= 3.2, < 7.0)
rake (>= 10.4, < 14.0)
ast (2.4.1)
attr_encrypted (3.1.0)
encryptor (~> 3.0.0)
attr_required (1.0.1)
autoprefixer-rails (10.0.1.0)
execjs
@ -220,6 +233,7 @@ GEM
em-websocket (0.5.1)
eventmachine (>= 0.12.9)
http_parser.rb (~> 0.6.0)
encryptor (3.0.0)
equalizer (0.0.11)
erubi (1.9.0)
erubis (2.7.0)
@ -407,7 +421,6 @@ GEM
momentjs-rails (2.20.1)
railties (>= 3.1)
multi_json (1.15.0)
multi_xml (0.6.0)
multipart-post (2.1.1)
mustermann (1.1.1)
ruby2_keywords (~> 0.0.1)
@ -419,24 +432,6 @@ GEM
notiffany (0.1.3)
nenv (~> 0.1)
shellany (~> 0.0)
oauth2 (1.4.4)
faraday (>= 0.8, < 2.0)
jwt (>= 1.0, < 3.0)
multi_json (~> 1.3)
multi_xml (~> 0.5)
rack (>= 1.2, < 3)
omniauth (1.9.1)
hashie (>= 3.4.6)
rack (>= 1.6.2, < 3)
omniauth-github (1.4.0)
omniauth (~> 1.5)
omniauth-oauth2 (>= 1.4.0, < 2.0)
omniauth-oauth2 (1.6.0)
oauth2 (~> 1.1)
omniauth (~> 1.9)
omniauth-rails_csrf_protection (0.1.2)
actionpack (>= 4.2)
omniauth (>= 1.3.1)
open4 (1.3.4)
openid_connect (1.1.8)
activemodel
@ -573,7 +568,13 @@ GEM
builder (>= 3.0)
dry-inflector (~> 0.1)
rubyzip (>= 1.0)
rotp (4.1.0)
addressable (~> 2.5)
rouge (3.17.0)
rqrcode (1.1.2)
chunky_png (~> 1.0)
rqrcode_core (~> 0.1)
rqrcode_core (0.1.2)
rspec (3.9.0)
rspec-core (~> 3.9.0)
rspec-expectations (~> 3.9.0)
@ -798,6 +799,7 @@ DEPENDENCIES
devise
devise-async
devise-i18n
devise-two-factor!
discard
dotenv-rails
factory_bot
@ -833,8 +835,6 @@ DEPENDENCIES
logstash-event
mailjet
mina!
omniauth-github
omniauth-rails_csrf_protection (~> 0.1)
openid_connect
pg
phonelib
@ -853,6 +853,7 @@ DEPENDENCIES
rake-progressbar
react-rails
rgeo-geojson
rqrcode
rspec-rails
rspec_junit_formatter
rubocop

View file

@ -1,16 +0,0 @@
class Administrations::OmniauthCallbacksController < Devise::OmniauthCallbacksController
def github
administration = Administration.from_omniauth(request.env["omniauth.auth"])
if administration.present?
sign_in administration
redirect_to manager_administrateurs_path
else
flash[:alert] = "Compte GitHub non autorisé"
redirect_to root_path
end
end
def failure
redirect_to root_path
end
end

View file

@ -1,12 +0,0 @@
class Administrations::SessionsController < ApplicationController
def new
end
def destroy
if administration_signed_in?
sign_out :administration
end
redirect_to root_path
end
end

View file

@ -13,6 +13,7 @@ class ApplicationController < ActionController::Base
before_action :set_raven_context
before_action :redirect_if_untrusted
before_action :reject, if: -> { feature_enabled?(:maintenance_mode) }
before_action :configure_permitted_parameters, if: :devise_controller?
before_action :staging_authenticate
before_action :set_active_storage_host
@ -105,6 +106,10 @@ class ApplicationController < ActionController::Base
stored_location_for(:user) || super
end
def configure_permitted_parameters
devise_parameter_sanitizer.permit(:sign_in, keys: [:otp_attempt])
end
private
def set_current_roles
@ -137,7 +142,7 @@ class ApplicationController < ActionController::Base
current_user,
current_instructeur,
current_administrateur,
current_administration
current_super_admin
].compact.map { |role| role.class.name }
roles.any? ? roles.join(', ') : 'Guest'
@ -175,11 +180,11 @@ class ApplicationController < ActionController::Base
authorized_request =
request.path_info == '/' ||
request.path_info.start_with?('/manager') ||
request.path_info.start_with?('/administrations')
request.path_info.start_with?('/super_admins')
api_request = request.path_info.start_with?('/api/')
if administration_signed_in? || authorized_request
if super_admin_signed_in? || authorized_request
flash.now.alert = MAINTENANCE_MESSAGE
elsif api_request
render json: { error: MAINTENANCE_MESSAGE }.to_json, status: :service_unavailable

View file

@ -1,7 +1,7 @@
module Manager
class AdministrateursController < Manager::ApplicationController
def create
administrateur = current_administration.invite_admin(create_administrateur_params[:email])
administrateur = current_super_admin.invite_admin(create_administrateur_params[:email])
if administrateur.errors.empty?
flash.notice = "Administrateur créé"
@ -14,7 +14,7 @@ module Manager
end
def reinvite
Administrateur.find_inactive_by_id(params[:id]).user.invite_administrateur!(current_administration.id)
Administrateur.find_inactive_by_id(params[:id]).user.invite_administrateur!(current_super_admin.id)
flash.notice = "Invitation renvoyée"
redirect_to manager_administrateur_path(params[:id])
end
@ -24,7 +24,7 @@ module Manager
administrateur.delete_and_transfer_services
logger.info("L'administrateur #{administrateur.id} est supprimé par #{current_administration.id}")
logger.info("L'administrateur #{administrateur.id} est supprimé par #{current_super_admin.id}")
flash[:notice] = "L'administrateur #{administrateur.id} est supprimé"
redirect_to manager_administrateurs_path

View file

@ -1,6 +1,6 @@
module Manager
class ApplicationController < Administrate::ApplicationController
before_action :authenticate_administration!
before_action :authenticate_super_admin!
before_action :default_params
def default_params
@ -12,11 +12,13 @@ module Manager
protected
def authenticate_administration!
if administration_signed_in?
def authenticate_super_admin!
if super_admin_signed_in? && current_super_admin.otp_required_for_login?
super
elsif super_admin_signed_in?
redirect_to edit_super_admin_otp_path
else
redirect_to manager_sign_in_path
redirect_to new_super_admin_session_path
end
end

View file

@ -5,12 +5,12 @@ module Manager
end
def create_administrateur
administrateur = current_administration.invite_admin(create_administrateur_params[:email])
administrateur = current_super_admin.invite_admin(create_administrateur_params[:email])
if administrateur.errors.empty?
PipedriveAcceptsDealsJob.perform_later(
create_administrateur_params[:person_id],
current_administration.id,
current_super_admin.id,
create_administrateur_params[:stage_id]
)
@ -26,7 +26,7 @@ module Manager
def refuse_administrateur
PipedriveRefusesDealsJob.perform_later(
refuse_administrateur_params[:person_id],
current_administration.id
current_super_admin.id
)
AdministrationMailer

View file

@ -22,9 +22,9 @@ module Manager
def discard
dossier = Dossier.find(params[:id])
dossier.discard_and_keep_track!(current_administration, :manager_request)
dossier.discard_and_keep_track!(current_super_admin, :manager_request)
logger.info("Le dossier #{dossier.id} est supprimé par #{current_administration.email}")
logger.info("Le dossier #{dossier.id} est supprimé par #{current_super_admin.email}")
flash[:notice] = "Le dossier #{dossier.id} a été supprimé."
redirect_to manager_dossier_path(dossier)
@ -32,7 +32,7 @@ module Manager
def restore
dossier = Dossier.with_discarded.find(params[:id])
dossier.restore(current_administration)
dossier.restore(current_super_admin)
flash[:notice] = "Le dossier #{dossier.id} a été restauré."
@ -41,9 +41,9 @@ module Manager
def repasser_en_instruction
dossier = Dossier.find(params[:id])
dossier.repasser_en_instruction(current_administration)
dossier.repasser_en_instruction(current_super_admin)
logger.info("Le dossier #{dossier.id} est repassé en instruction par #{current_administration.email}")
logger.info("Le dossier #{dossier.id} est repassé en instruction par #{current_super_admin.email}")
flash[:notice] = "Le dossier #{dossier.id} est repassé en instruction."
redirect_to manager_dossier_path(dossier)

View file

@ -15,7 +15,7 @@ module Manager
end
instructeur.destroy!
logger.info("L'instructeur #{instructeur.id} est supprimé par #{current_administration.id}")
logger.info("L'instructeur #{instructeur.id} est supprimé par #{current_super_admin.id}")
flash[:notice] = "L'instructeur #{instructeur.id} est supprimé"
redirect_to manager_instructeurs_path

View file

@ -23,16 +23,16 @@ module Manager
end
def discard
procedure.discard_and_keep_track!(current_administration)
procedure.discard_and_keep_track!(current_super_admin)
logger.info("La démarche #{procedure.id} est supprimée par #{current_administration.email}")
logger.info("La démarche #{procedure.id} est supprimée par #{current_super_admin.email}")
flash[:notice] = "La démarche #{procedure.id} a été supprimée."
redirect_to manager_procedure_path(procedure)
end
def restore
procedure.restore(current_administration)
procedure.restore(current_super_admin)
flash[:notice] = "La démarche #{procedure.id} a été restauré."

View file

@ -39,9 +39,9 @@ module Manager
if !user.can_be_deleted?
fail "Impossible de supprimer cet utilisateur. Il a des dossiers en instruction ou il est administrateur."
end
user.delete_and_keep_track_dossiers(current_administration)
user.delete_and_keep_track_dossiers(current_super_admin)
logger.info("L'utilisateur #{user.id} est supprimé par #{current_administration.id}")
logger.info("L'utilisateur #{user.id} est supprimé par #{current_super_admin.id}")
flash[:notice] = "L'utilisateur #{user.id} est supprimé"
redirect_to manager_users_path

View file

@ -8,7 +8,7 @@ class RootController < ApplicationController
return redirect_to instructeur_procedures_path
elsif user_signed_in?
return redirect_to dossiers_path
elsif administration_signed_in?
elsif super_admin_signed_in?
return redirect_to manager_root_path
end

View file

@ -1,5 +1,5 @@
class StatsController < ApplicationController
before_action :authenticate_administration!, only: [:download]
before_action :authenticate_super_admin!, only: [:download]
MEAN_NUMBER_OF_CHAMPS_IN_A_FORM = 24.0
@ -34,7 +34,7 @@ class StatsController < ApplicationController
@dossiers_cumulative = stat.dossiers_cumulative
@dossiers_in_the_last_4_months = stat.dossiers_in_the_last_4_months
if administration_signed_in?
if super_admin_signed_in?
@dossier_instruction_mean_time = Rails.cache.fetch("dossier_instruction_mean_time", expires_in: 1.day) do
dossier_instruction_mean_time(dossiers)
end
@ -201,7 +201,7 @@ class StatsController < ApplicationController
end
def max_date
if administration_signed_in?
if super_admin_signed_in?
Time.zone.now
else
Time.zone.now.beginning_of_month - 1.second

View file

@ -0,0 +1,6 @@
class Administrations::PasswordsController < Devise::PasswordsController
def update
super
self.resource.disable_otp!
end
end

View file

@ -0,0 +1,2 @@
class Administrations::SessionsController < Devise::SessionsController
end

View file

@ -0,0 +1,28 @@
class SuperAdminsController < ApplicationController
before_action :authenticate_super_admin!
def edit_otp
end
def enable_otp
current_super_admin.enable_otp!
@qrcode = generate_qr_code
sign_out :super_admin
end
protected
def authenticate_super_admin!
if !super_admin_signed_in?
redirect_to root_path
end
end
private
def generate_qr_code
issuer = 'DSManager'
label = "#{issuer}:#{current_super_admin.email}"
RQRCode::QRCode.new(current_super_admin.otp_provisioning_uri(label, issuer: issuer))
end
end

View file

@ -1,40 +0,0 @@
# == Schema Information
#
# Table name: administrations
#
# id :integer not null, primary key
# current_sign_in_at :datetime
# current_sign_in_ip :string
# email :string default(""), not null
# encrypted_password :string default(""), not null
# failed_attempts :integer default(0), not null
# last_sign_in_at :datetime
# last_sign_in_ip :string
# locked_at :datetime
# remember_created_at :datetime
# reset_password_sent_at :datetime
# reset_password_token :string
# sign_in_count :integer default(0), not null
# unlock_token :string
# created_at :datetime
# updated_at :datetime
#
class Administration < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :rememberable, :trackable, :validatable, :omniauthable, :lockable, :async, omniauth_providers: [:github]
def self.from_omniauth(params)
find_by(email: params["info"]["email"])
end
def invite_admin(email)
user = User.create_or_promote_to_administrateur(email, SecureRandom.hex)
if user.valid?
user.invite_administrateur!(id)
end
user
end
end

59
app/models/super_admin.rb Normal file
View file

@ -0,0 +1,59 @@
# == Schema Information
#
# Table name: super_admins
#
# id :integer not null, primary key
# consumed_timestep :integer
# current_sign_in_at :datetime
# current_sign_in_ip :string
# email :string default(""), not null
# encrypted_otp_secret :string
# encrypted_otp_secret_iv :string
# encrypted_otp_secret_salt :string
# encrypted_password :string default(""), not null
# failed_attempts :integer default(0), not null
# last_sign_in_at :datetime
# last_sign_in_ip :string
# locked_at :datetime
# otp_required_for_login :boolean
# remember_created_at :datetime
# reset_password_sent_at :datetime
# reset_password_token :string
# sign_in_count :integer default(0), not null
# unlock_token :string
# created_at :datetime
# updated_at :datetime
#
class SuperAdmin < ApplicationRecord
devise :rememberable, :trackable, :validatable, :lockable, :async, :recoverable,
:two_factor_authenticatable, :otp_secret_encryption_key => Rails.application.secrets.otp_secret_key
def enable_otp!
self.otp_secret = SuperAdmin.generate_otp_secret
self.otp_required_for_login = true
save!
end
def disable_otp!
self.assign_attributes(
{
encrypted_otp_secret: nil,
encrypted_otp_secret_iv: nil,
encrypted_otp_secret_salt: nil,
consumed_timestep: nil,
otp_required_for_login: false
}
)
save!
end
def invite_admin(email)
user = User.create_or_promote_to_administrateur(email, SecureRandom.hex)
if user.valid?
user.invite_administrateur!(id)
end
user
end
end

View file

@ -9,7 +9,7 @@ class OperationAuthorSerializer < ActiveModel::Serializer
"Instructeur##{object.id}"
when Administrateur
"Administrateur##{object.id}"
when Administration
when SuperAdmin
"Manager##{object.id}"
else
nil

View file

@ -0,0 +1,6 @@
.super-admin.flex.justify-center
%div
%h2.huge-title Espace Manager
%p Munissez-vous de votre téléphone sur lequel vous avez installé une application cliente 2FA (Google Authenticator, Authy, AndOTP, ...)
%br
%p= link_to "Activer l'authentification double-facteur", enable_administration_otp_path, method: :put, class: 'button primary'

View file

@ -0,0 +1,12 @@
.container
%p
%strong Si vous n'effectuez pas cette étape maintenant, vous ne pourrez plus vous connecter au manager !
%p Depuis votre téléphone, lancez votre application cliente 2FA et scannez ce QRCode afin d'ajouter votre compte DSManager. Votre application vous fournira ensuite à chaque connexion au manager le code otp à saisir.
%br
= raw @qrcode.as_svg(module_size: 6)
%br
%p
Après avoir scanné le QRCode ci-dessus, nous vous invitons à
= link_to 'accéder au Manager' , manager_root_path

View file

@ -0,0 +1,22 @@
- content_for(:title, 'Changement de mot de passe')
- content_for :footer do
= render partial: 'root/footer'
.container.devise-container
.one-column-centered
= devise_error_messages!
= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :patch, class: 'form' }) do |f|
%h1 Changement de mot de passe
= f.hidden_field :reset_password_token
= f.label 'Nouveau mot de passe'
= f.password_field :password, autofocus: true, autocomplete: 'off'
= f.label 'Confirmez le nouveau mot de passe'
= f.password_field :password_confirmation, autocomplete: 'off'
= f.submit 'Changer le mot de passe', class: 'button primary'

View file

@ -0,0 +1,17 @@
- content_for(:title, 'Mot de passe oublié')
- content_for :footer do
= render partial: 'root/footer'
.container.devise-container
.one-column-centered
= devise_error_messages!
= form_for(resource, as: resource_name, url: password_path(resource_name), html: { class: 'form' }) do |f|
%h1 Mot de passe oublié
= f.label :email, 'Email'
= f.email_field :email, autofocus: true
= f.submit 'Réinitialiser', class: 'button primary'

View file

@ -1,6 +1,21 @@
.super-admin.flex.justify-center
%div
%h2 Espace Admin
= link_to administration_github_omniauth_authorize_path, method: :post, class: "button large" do
%span.icon.lock
Connexion avec GitHub
%h2.huge-title Espace Manager
.auth-form.sign-in-form
= form_for Administration.new, url: administration_session_path, html: { class: "form" } do |f|
%h1 Connectez-vous
= f.label :email, "Email (nom@site.com)"
= f.text_field :email, type: :email, autocomplete: 'username', autofocus: true
= f.label :password, "Mot de passe (#{PASSWORD_MIN_LENGTH} caractères minimum)"
= f.password_field :password, autocomplete: 'current-password'
= f.label :otp_attempt, 'Code OTP (uniquement si vous avez déjà activé 2FA)'
= f.text_field :otp_attempt
.auth-options
.text-right
= link_to "Mot de passe oublié ou réinitialisation 2FA ?", new_administration_password_path, class: "link"
= f.submit "Se connecter", class: "button large primary expand"

View file

@ -6,7 +6,7 @@
%li
.menu-item{ title: current_email }
= current_email
- if administration_signed_in?
- if super_admin_signed_in?
%li
= link_to manager_root_path, class: "menu-item menu-link" do
= image_tag "icons/super-admin.svg", alt: ''

View file

@ -8,7 +8,7 @@ as defined by the routes in the `admin/` namespace
%>
<nav class="navigation" role="navigation">
<%= link_to "Se déconnecter", manager_sign_out_path, method: :delete, class: "navigation__link" %>
<%= link_to "Se déconnecter", destroy_super_admin_session_path, method: :delete, class: "navigation__link" %>
<hr />

View file

@ -88,7 +88,7 @@
.chart.cumulative-dossiers-chart.hidden
= area_chart @dossiers_cumulative
- if administration_signed_in?
- if super_admin_signed_in?
.stat-card.stat-card-half.pull-left
%span.stat-card-title Temps de traitement moyen d'un dossier
@ -107,7 +107,7 @@
.clearfix
- if administration_signed_in?
- if super_admin_signed_in?
%h2.new-h2 Téléchargement
= link_to "Télécharger les statistiques (CSV)", stats_download_path(format: :csv), class: 'button secondary'

View file

@ -17,6 +17,9 @@ SOURCE="tps_local"
SECRET_KEY_BASE="05a2d479d8e412198dabd08ef0eee9d6e180f5cbb48661a35fd1cae287f0a93d40b5f1da08f06780d698bbd458a0ea97f730f83ee780de5d4e31f649a0130cf0"
SIGNING_KEY="aef3153a9829fa4ba10acb02927ac855df6b92795b1ad265d654443c4b14a017"
# Clé de chiffrement OTP, pour 2FA
OTP_SECRET_KEY=""
# Database
DB_DATABASE="tps_development"
DB_HOST="localhost"
@ -42,10 +45,6 @@ FC_PARTICULIER_ID=""
FC_PARTICULIER_SECRET=""
FC_PARTICULIER_BASE_URL=""
# Service externe: Authentification pour manager (auth Github obligatoire), permet d'accéder à /manager
GITHUB_CLIENT_ID=""
GITHUB_CLIENT_SECRET=""
# Service externe: Support Utilisateur HelpScout | Spécifique démarches-simplifiées.fr
HELPSCOUT_MAILBOX_ID=""
HELPSCOUT_CLIENT_ID=""

View file

@ -232,21 +232,12 @@ Devise.setup do |config|
# The default HTTP method used to sign out a resource. Default is :delete.
config.sign_out_via = :delete
# ==> OmniAuth
# Add a new OmniAuth provider. Check the wiki for more information on setting
# up on your models and hooks.
if !Rails.env.test?
config.omniauth :github, Rails.application.secrets.github[:client_id], Rails.application.secrets.github[:client_secret], scope: 'user:email'
end
# ==> Warden configuration
# If you want to use other strategies, that are not supported by Devise, or
# change the failure app, you can configure them inside the config.warden block.
#
config.warden do |manager|
# manager.intercept_401 = false
# manager.default_strategies(scope: :user).unshift :some_external_strategy
# manager.failure_app = User::CustomFailure
manager.default_strategies(:scope => :administration).unshift :two_factor_authenticatable
end
# ==> Mountable engine configurations

View file

@ -1,4 +0,0 @@
# OmniAuth GET requests may be vulnerable to CSRF.
# Ensure that OmniAuth only uses POST requests.
# See https://github.com/omniauth/omniauth/wiki/Resolving-CVE-2015-9284
OmniAuth.config.allowed_request_methods = [:post]

View file

@ -3,8 +3,6 @@ Rails.application.routes.draw do
# Manager
#
get 'manager/sign_in' => 'administrations/sessions#new'
delete 'manager/sign_out' => 'administrations/sessions#destroy'
namespace :manager do
resources :procedures, only: [:index, :show] do
post 'whitelist', on: :member
@ -77,11 +75,13 @@ Rails.application.routes.draw do
# Authentication
#
devise_for :administrations,
skip: [:password, :registrations, :sessions],
controllers: {
omniauth_callbacks: 'administrations/omniauth_callbacks'
}
devise_for :super_admins, skip: [:registrations], controllers: {
sessions: 'super_admins/sessions',
passwords: 'super_admins/passwords'
}
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'
devise_for :users, controllers: {
sessions: 'users/sessions',

View file

@ -12,6 +12,7 @@
defaults: &defaults
secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
signing_key: <%= ENV["SIGNING_KEY"] %>
otp_secret_key: <%= ENV["OTP_SECRET_KEY"] %>
basic_auth:
username: <%= ENV['BASIC_AUTH_USERNAME'] %>
password: <%= ENV['BASIC_AUTH_PASSWORD'] %>
@ -23,9 +24,6 @@ defaults: &defaults
token_endpoint: <%= ENV['FC_PARTICULIER_BASE_URL'] %>/api/v1/token
userinfo_endpoint: <%= ENV['FC_PARTICULIER_BASE_URL'] %>/api/v1/userinfo
logout_endpoint: <%= ENV['FC_PARTICULIER_BASE_URL'] %>/api/v1/logout
github:
client_id: <%= ENV['GITHUB_CLIENT_ID'] %>
client_secret: <%= ENV['GITHUB_CLIENT_SECRET'] %>
mailjet:
api_key: <%= ENV['MAILJET_API_KEY'] %>
secret_key: <%= ENV['MAILJET_SECRET_KEY'] %>
@ -76,6 +74,7 @@ test:
<<: *defaults
secret_key_base: aa52abc3f3a629d04a61e9899a24c12f52b24c679cbf45f8ec0cdcc64ab9526d673adca84212882dff3911ac98e0c32ec4729ca7b3429ba18ef4dfd1bd18bc7a
signing_key: aef3153a9829fa4ba10acb02927ac855df6b92795b1ad265d654443c4b14a017
otp_secret_key: 78ddda3679dc0ba2c99f50bcff04f49d862358dbeb7ead50368fdd6de14392be884ee10a204a0375b4b382e1a842fafe40d7858b7ab4796ec3a67c518d31112b
api_entreprise:
key: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ik9oIHllYWgiLCJpYXQiOjE1MTYyMzkwMjJ9.f06sBo3q2Yxnw_TYPFUEs0CozBmcV-XniH_DeKNWzKE"
pipedrive:

View file

@ -0,0 +1,9 @@
class AddDeviseTwoFactorToAdministrations < ActiveRecord::Migration[6.0]
def change
add_column :administrations, :encrypted_otp_secret, :string
add_column :administrations, :encrypted_otp_secret_iv, :string
add_column :administrations, :encrypted_otp_secret_salt, :string
add_column :administrations, :consumed_timestep, :integer
add_column :administrations, :otp_required_for_login, :boolean
end
end

View file

@ -0,0 +1,5 @@
class RenameAdministrationsToSuperAdmins < ActiveRecord::Migration[6.0]
def change
rename_table :administrations, :super_admins
end
end

View file

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2020_10_02_124154) do
ActiveRecord::Schema.define(version: 2020_11_05_131443) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -74,27 +74,6 @@ ActiveRecord::Schema.define(version: 2020_10_02_124154) do
t.index ["procedure_id"], name: "index_administrateurs_procedures_on_procedure_id"
end
create_table "administrations", id: :serial, force: :cascade do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
t.string "reset_password_token"
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
t.integer "sign_in_count", default: 0, null: false
t.datetime "current_sign_in_at"
t.datetime "last_sign_in_at"
t.string "current_sign_in_ip"
t.string "last_sign_in_ip"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "failed_attempts", default: 0, null: false
t.string "unlock_token"
t.datetime "locked_at"
t.index ["email"], name: "index_administrations_on_email", unique: true
t.index ["reset_password_token"], name: "index_administrations_on_reset_password_token", unique: true
t.index ["unlock_token"], name: "index_administrations_on_unlock_token", unique: true
end
create_table "assign_tos", id: :serial, force: :cascade do |t|
t.integer "instructeur_id"
t.integer "procedure_id"
@ -601,6 +580,32 @@ ActiveRecord::Schema.define(version: 2020_10_02_124154) do
t.datetime "updated_at", precision: 6, null: false
end
create_table "super_admins", id: :serial, force: :cascade do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
t.string "reset_password_token"
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
t.integer "sign_in_count", default: 0, null: false
t.datetime "current_sign_in_at"
t.datetime "last_sign_in_at"
t.string "current_sign_in_ip"
t.string "last_sign_in_ip"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "failed_attempts", default: 0, null: false
t.string "unlock_token"
t.datetime "locked_at"
t.string "encrypted_otp_secret"
t.string "encrypted_otp_secret_iv"
t.string "encrypted_otp_secret_salt"
t.integer "consumed_timestep"
t.boolean "otp_required_for_login"
t.index ["email"], name: "index_super_admins_on_email", unique: true
t.index ["reset_password_token"], name: "index_super_admins_on_reset_password_token", unique: true
t.index ["unlock_token"], name: "index_super_admins_on_unlock_token", unique: true
end
create_table "task_records", id: false, force: :cascade do |t|
t.string "version", null: false
end

View file

@ -18,10 +18,12 @@ namespace :superadmin do
email = args[:email]
rake_puts "Creating Administration for #{email}"
a = Administration.new(email: email, password: Devise.friendly_token[0, 20])
a = Administration.new(email: email, password: Devise.friendly_token)
if a.save
rake_puts "#{a.email} created"
a.send_reset_password_instructions
rake_puts "Password reset instructions sent to #{a.email}"
else
rake_puts "An error occured: #{a.errors.full_messages}"
end

View file

@ -1,36 +0,0 @@
describe Administrations::OmniauthCallbacksController, type: :controller do
before(:each) do
@request.env["devise.mapping"] = Devise.mappings[:administration]
end
describe 'POST #github' do
let(:params) { { "info" => { "email" => email } } }
before do
allow(controller).to receive(:sign_in).and_return true
@request.env["omniauth.auth"] = params
end
subject { post :github }
context 'with an authorized email' do
let(:email) { "ivan@tps.fr" }
let(:administration) { create(:administration, email: email) }
before { administration }
it { is_expected.to redirect_to(manager_administrateurs_path) }
it do
expect(controller).to receive(:sign_in).with(administration)
subject
end
end
context 'with an unauthorized email' do
let(:email) { "michel@tps.fr" }
it { is_expected.to redirect_to(root_path) }
it do
expect(controller).to_not receive(:sign_in)
subject
end
end
end
end

View file

@ -15,7 +15,7 @@ describe ApplicationController, type: :controller do
let(:current_user) { nil }
let(:current_instructeur) { nil }
let(:current_administrateur) { nil }
let(:current_administration) { nil }
let(:current_super_admin) { nil }
let(:payload) { {} }
before do
@ -23,7 +23,7 @@ describe ApplicationController, type: :controller do
allow(@controller).to receive(:current_user).and_return(current_user)
expect(@controller).to receive(:current_instructeur).and_return(current_instructeur)
expect(@controller).to receive(:current_administrateur).and_return(current_administrateur)
expect(@controller).to receive(:current_administration).and_return(current_administration)
expect(@controller).to receive(:current_super_admin).and_return(current_super_admin)
allow(Raven).to receive(:user_context)
@controller.send(:set_raven_context)
@ -72,11 +72,11 @@ describe ApplicationController, type: :controller do
end
end
context 'when someone is logged as a user, instructeur, administrateur and administration' do
context 'when someone is logged as a user, instructeur, administrateur and super_admin' do
let(:current_user) { create(:user) }
let(:current_instructeur) { create(:instructeur) }
let(:current_administrateur) { create(:administrateur) }
let(:current_administration) { create(:administration) }
let(:current_super_admin) { create(:super_admin) }
it do
expect(Raven).to have_received(:user_context)
@ -93,7 +93,7 @@ describe ApplicationController, type: :controller do
user_agent: 'Rails Testing',
user_id: current_user.id,
user_email: current_user.email,
user_roles: 'User, Instructeur, Administrateur, Administration'
user_roles: 'User, Instructeur, Administrateur, SuperAdmin'
})
end
end
@ -109,7 +109,7 @@ describe ApplicationController, type: :controller do
@request.path_info = path_info
end
context 'when no administration is logged in' do
context 'when no super_admin is logged in' do
before { @controller.send(:reject) }
it { expect(@controller).to have_received(:sign_out).with(:user) }
@ -119,7 +119,7 @@ describe ApplicationController, type: :controller do
it { expect(@controller).to have_received(:redirect_to).with(root_path) }
context 'when the path is safe' do
['/', '/manager', '/administrations'].each do |path|
['/', '/manager', '/super_admins'].each do |path|
let(:path_info) { path }
it { expect(@controller).not_to have_received(:sign_out) }
@ -138,11 +138,11 @@ describe ApplicationController, type: :controller do
end
end
context 'when a administration is logged in' do
let(:current_administration) { create(:administration) }
context 'when a super_admin is logged in' do
let(:current_super_admin) { create(:super_admin) }
before do
sign_in(current_administration)
sign_in(current_super_admin)
@controller.send(:reject)
end

View file

@ -1,19 +1,29 @@
describe Manager::AdministrateursController, type: :controller do
let(:administration) { create(:administration) }
let(:super_admin) { create(:super_admin) }
let(:administrateur) { create(:administrateur) }
before do
sign_in administration
sign_in super_admin
end
describe '#show' do
render_views
let(:subject) { get :show, params: { id: administrateur.id } }
before do
get :show, params: { id: administrateur.id }
context 'with 2FA not enabled' do
let(:super_admin) { create(:super_admin, otp_required_for_login: false) }
it { expect(subject).to redirect_to(edit_super_admin_otp_path) }
end
it { expect(response.body).to include(administrateur.email) }
context 'with 2FA enabled' do
render_views
let(:super_admin) { create(:super_admin, otp_required_for_login: true) }
before do
subject
end
it { expect(response.body).to include(administrateur.email) }
end
end
describe 'GET #new' do

View file

@ -1,6 +1,6 @@
describe Manager::ApplicationController, type: :controller do
describe 'append_info_to_payload' do
let(:current_user) { create(:administration) }
let(:current_user) { create(:super_admin) }
let(:payload) { {} }
before do

View file

@ -1,9 +1,9 @@
describe Manager::DemandesController, type: :controller do
let(:administration) { create(:administration) }
let(:super_admin) { create(:super_admin) }
describe 'GET #index' do
before do
sign_in administration
sign_in super_admin
end
it "display pending demandes" do

View file

@ -1,9 +1,9 @@
describe Manager::DossiersController, type: :controller do
let(:administration) { create :administration }
let(:super_admin) { create :super_admin }
let(:deleted_dossier) { DeletedDossier.find_by(dossier_id: dossier) }
let(:operations) { dossier.dossier_operation_logs.map(&:operation).map(&:to_sym) }
before { sign_in administration }
before { sign_in super_admin }
describe '#discard' do
let(:dossier) { create(:dossier, :en_construction) }
@ -23,7 +23,7 @@ describe Manager::DossiersController, type: :controller do
let(:dossier) { create(:dossier, :en_construction) }
before do
dossier.discard_and_keep_track!(administration, :manager_request)
dossier.discard_and_keep_track!(super_admin, :manager_request)
post :restore, params: { id: dossier.id }
dossier.reload

View file

@ -1,12 +1,12 @@
describe Manager::InstructeursController, type: :controller do
let(:administration) { create(:administration) }
let(:super_admin) { create(:super_admin) }
let(:instructeur) { create(:instructeur) }
describe '#show' do
render_views
before do
sign_in(administration)
sign_in(super_admin)
get :show, params: { id: instructeur.id }
end
@ -14,7 +14,7 @@ describe Manager::InstructeursController, type: :controller do
end
describe '#delete' do
before { sign_in administration }
before { sign_in super_admin }
subject { delete :delete, params: { id: instructeur.id } }

View file

@ -1,7 +1,7 @@
describe Manager::ProceduresController, type: :controller do
let(:administration) { create :administration }
let(:super_admin) { create :super_admin }
before { sign_in administration }
before { sign_in super_admin }
describe '#whitelist' do
let(:procedure) { create(:procedure) }
@ -52,7 +52,7 @@ describe Manager::ProceduresController, type: :controller do
let(:operations) { dossier.dossier_operation_logs.map(&:operation).map(&:to_sym) }
before do
procedure.discard_and_keep_track!(administration)
procedure.discard_and_keep_track!(super_admin)
post :restore, params: { id: procedure.id }
procedure.reload

View file

@ -1,14 +1,14 @@
describe Manager::UsersController, type: :controller do
let(:administration) { create(:administration) }
let(:super_admin) { create(:super_admin) }
describe '#show' do
render_views
let(:administration) { create(:administration) }
let(:super_admin) { create(:super_admin) }
let(:user) { create(:user) }
before do
sign_in(administration)
sign_in(super_admin)
get :show, params: { id: user.id }
end
@ -19,7 +19,7 @@ describe Manager::UsersController, type: :controller do
let!(:user) { create(:user, email: 'ancien.email@domaine.fr') }
before {
sign_in administration
sign_in super_admin
}
subject { patch :update, params: { id: user.id, user: { email: nouvel_email } } }
@ -48,7 +48,7 @@ describe Manager::UsersController, type: :controller do
describe '#delete' do
let!(:user) { create(:user) }
before { sign_in administration }
before { sign_in super_admin }
subject { delete :delete, params: { id: user.id } }

View file

@ -32,7 +32,7 @@ describe RootController, type: :controller do
context 'when Administration is connected' do
before do
sign_in create(:administration)
sign_in create(:super_admin)
end
it { expect(subject).to redirect_to(manager_root_path) }

View file

@ -9,7 +9,7 @@ describe StatsController, type: :controller do
create(:procedure, created_at: 2.months.ago, updated_at: Time.zone.now)
@controller = StatsController.new
allow(@controller).to receive(:administration_signed_in?).and_return(false)
allow(@controller).to receive(:super_admin_signed_in?).and_return(false)
end
let(:association) { Procedure.all }
@ -33,7 +33,7 @@ describe StatsController, type: :controller do
@controller = StatsController.new
allow(@controller).to receive(:administration_signed_in?).and_return(true)
allow(@controller).to receive(:super_admin_signed_in?).and_return(true)
end
let (:association) { Procedure.all }
@ -64,7 +64,7 @@ describe StatsController, type: :controller do
let (:association) { Procedure.all }
context "while a super admin is logged in" do
before { allow(@controller).to receive(:administration_signed_in?).and_return(true) }
before { allow(@controller).to receive(:super_admin_signed_in?).and_return(true) }
subject { @controller.send(:cumulative_hash, association, :updated_at) }
@ -78,7 +78,7 @@ describe StatsController, type: :controller do
end
context "while a super admin is not logged in" do
before { allow(@controller).to receive(:administration_signed_in?).and_return(false) }
before { allow(@controller).to receive(:super_admin_signed_in?).and_return(false) }
subject { @controller.send(:cumulative_hash, association, :updated_at) }

View file

@ -1,7 +0,0 @@
FactoryBot.define do
sequence(:administration_email) { |n| "plop#{n}@plop.com" }
factory :administration do
email { generate(:administration_email) }
password { 'my-s3cure-p4ssword' }
end
end

View file

@ -0,0 +1,8 @@
FactoryBot.define do
sequence(:super_admin_email) { |n| "plop#{n}@plop.com" }
factory :super_admin do
email { generate(:super_admin_email) }
password { 'my-s3cure-p4ssword' }
otp_required_for_login { true }
end
end

View file

@ -1,11 +1,11 @@
feature 'As an administrateur', js: true do
let(:administration) { create(:administration) }
let(:super_admin) { create(:super_admin) }
let(:admin_email) { 'new_admin@gouv.fr' }
let(:new_admin) { Administrateur.by_email(admin_email) }
before do
perform_enqueued_jobs do
administration.invite_admin(admin_email)
super_admin.invite_admin(admin_email)
end
end

View file

@ -1,37 +0,0 @@
describe Administration, type: :model do
describe '#invite_admin' do
let(:administration) { create :administration }
let(:valid_email) { 'paul@tps.fr' }
subject { administration.invite_admin(valid_email) }
it {
user = subject
expect(user.errors).to be_empty
expect(user).to be_persisted
}
it { expect(administration.invite_admin(nil).errors).not_to be_empty }
it { expect(administration.invite_admin('toto').errors).not_to be_empty }
it 'creates a corresponding user account for the email' do
subject
user = User.find_by(email: valid_email)
expect(user).to be_present
end
it 'creates a corresponding instructeur account for the email' do
subject
instructeur = Instructeur.by_email(valid_email)
expect(instructeur).to be_present
end
context 'when there already is a user account with the same email' do
before { create(:user, email: valid_email) }
it 'still creates an admin account' do
expect(subject.errors).to be_empty
expect(subject).to be_persisted
end
end
end
end

View file

@ -1247,7 +1247,7 @@ describe Dossier do
end
describe 'discarded_brouillon_expired and discarded_en_construction_expired' do
let(:administration) { create(:administration) }
let(:super_admin) { create(:super_admin) }
before do
create(:dossier)
@ -1259,8 +1259,8 @@ describe Dossier do
create(:dossier).discard!
create(:dossier, :en_construction).discard!
create(:dossier).procedure.discard_and_keep_track!(administration)
create(:dossier, :en_construction).procedure.discard_and_keep_track!(administration)
create(:dossier).procedure.discard_and_keep_track!(super_admin)
create(:dossier, :en_construction).procedure.discard_and_keep_track!(super_admin)
end
Timecop.travel(1.week.ago) do
create(:dossier).discard!

View file

@ -845,7 +845,7 @@ describe Procedure do
end
describe "#discard_and_keep_track!" do
let(:administration) { create(:administration) }
let(:super_admin) { create(:super_admin) }
let(:procedure) { create(:procedure) }
let!(:dossier) { create(:dossier, procedure: procedure) }
let!(:dossier2) { create(:dossier, procedure: procedure) }
@ -857,7 +857,7 @@ describe Procedure do
context "when discarding procedure" do
before do
instructeur.followed_dossiers << dossier
procedure.discard_and_keep_track!(administration)
procedure.discard_and_keep_track!(super_admin)
instructeur.reload
end

View file

@ -0,0 +1,64 @@
describe SuperAdmin, type: :model do
describe '#invite_admin' do
let(:super_admin) { create :super_admin }
let(:valid_email) { 'paul@tps.fr' }
subject { super_admin.invite_admin(valid_email) }
it {
user = subject
expect(user.errors).to be_empty
expect(user).to be_persisted
}
it { expect(super_admin.invite_admin(nil).errors).not_to be_empty }
it { expect(super_admin.invite_admin('toto').errors).not_to be_empty }
it 'creates a corresponding user account for the email' do
subject
user = User.find_by(email: valid_email)
expect(user).to be_present
end
it 'creates a corresponding instructeur account for the email' do
subject
instructeur = Instructeur.by_email(valid_email)
expect(instructeur).to be_present
end
context 'when there already is a user account with the same email' do
before { create(:user, email: valid_email) }
it 'still creates an admin account' do
expect(subject.errors).to be_empty
expect(subject).to be_persisted
end
end
end
describe 'enable_otp!' do
let(:super_admin) { create(:super_admin, otp_required_for_login: false) }
let(:subject) { super_admin.enable_otp! }
it 'updates otp_required_for_login' do
expect { subject }.to change { super_admin.otp_required_for_login? }.from(false).to(true)
end
it 'updates otp_secret' do
expect { subject }.to change { super_admin.otp_secret }
end
end
describe 'disable_otp!' do
let(:super_admin) { create(:super_admin, otp_required_for_login: true) }
let(:subject) { super_admin.disable_otp! }
it 'updates otp_required_for_login' do
expect { subject }.to change { super_admin.otp_required_for_login? }.from(true).to(false)
end
it 'nullifies otp_secret' do
super_admin.enable_otp!
expect { subject }.to change { super_admin.reload.otp_secret }.to(nil)
end
end
end

View file

@ -164,7 +164,7 @@ describe User, type: :model do
end
describe 'invite_administrateur!' do
let(:administration) { create(:administration) }
let(:super_admin) { create(:super_admin) }
let(:administrateur) { create(:administrateur) }
let(:user) { administrateur.user }
@ -172,12 +172,12 @@ describe User, type: :model do
before { allow(AdministrationMailer).to receive(:invite_admin).and_return(mailer_double) }
subject { user.invite_administrateur!(administration.id) }
subject { user.invite_administrateur!(super_admin.id) }
context 'when the user is inactif' do
before { subject }
it { expect(AdministrationMailer).to have_received(:invite_admin).with(user, kind_of(String), administration.id) }
it { expect(AdministrationMailer).to have_received(:invite_admin).with(user, kind_of(String), super_admin.id) }
end
context 'when the user is actif' do
@ -187,7 +187,7 @@ describe User, type: :model do
end
it 'receives an invitation to update its password' do
expect(AdministrationMailer).to have_received(:invite_admin).with(user, kind_of(String), administration.id)
expect(AdministrationMailer).to have_received(:invite_admin).with(user, kind_of(String), super_admin.id)
end
end
end
@ -245,13 +245,13 @@ describe User, type: :model do
end
describe '#delete_and_keep_track_dossiers' do
let(:administration) { create(:administration) }
let(:super_admin) { create(:super_admin) }
let(:user) { create(:user) }
context 'with a dossier in instruction' do
let!(:dossier_en_instruction) { create(:dossier, :en_instruction, user: user) }
it 'raises' do
expect { user.delete_and_keep_track_dossiers(administration) }.to raise_error(RuntimeError)
expect { user.delete_and_keep_track_dossiers(super_admin) }.to raise_error(RuntimeError)
end
end
@ -261,7 +261,7 @@ describe User, type: :model do
context 'without a discarded dossier' do
it "keep track of dossiers and delete user" do
user.delete_and_keep_track_dossiers(administration)
user.delete_and_keep_track_dossiers(super_admin)
expect(DeletedDossier.find_by(dossier_id: dossier_en_construction)).to be_present
expect(DeletedDossier.find_by(dossier_id: dossier_brouillon)).to be_nil
@ -278,8 +278,8 @@ describe User, type: :model do
end
it "keep track of dossiers and delete user" do
dossier_cache.discard_and_keep_track!(administration, :user_request)
user.delete_and_keep_track_dossiers(administration)
dossier_cache.discard_and_keep_track!(super_admin, :user_request)
user.delete_and_keep_track_dossiers(super_admin)
expect(DeletedDossier.find_by(dossier_id: dossier_en_construction)).to be_present
expect(DeletedDossier.find_by(dossier_id: dossier_brouillon)).to be_nil
@ -287,8 +287,8 @@ describe User, type: :model do
end
it "doesn't destroy dossiers of another user" do
dossier_cache.discard_and_keep_track!(administration, :user_request)
user.delete_and_keep_track_dossiers(administration)
dossier_cache.discard_and_keep_track!(super_admin, :user_request)
user.delete_and_keep_track_dossiers(super_admin)
expect(Dossier.find_by(id: dossier_from_another_user.id)).to be_present
end