Merge pull request #7557 from mfo/US/super-admin-can-not-export

feat(export/archive): prevent creation of archives from user having a SuperAdmin user with the same email
This commit is contained in:
mfo 2022-08-23 14:38:41 +02:00 committed by GitHub
commit ee0280de17
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 403 additions and 110 deletions

View file

@ -1,12 +1,12 @@
module Administrateurs module Administrateurs
class AdministrateurController < ApplicationController class AdministrateurController < ApplicationController
before_action :authenticate_administrateur! before_action :authenticate_administrateur!
helper_method :administrateur_as_manager?
def retrieve_procedure def retrieve_procedure
id = params[:procedure_id] || params[:id] id = params[:procedure_id] || params[:id]
@procedure = current_administrateur.procedures.find(id) @procedure = current_administrateur.procedures.find(id)
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
flash.alert = 'Démarche inexistante' flash.alert = 'Démarche inexistante'
redirect_to admin_procedures_path, status: 404 redirect_to admin_procedures_path, status: 404
@ -31,5 +31,20 @@ module Administrateurs
@procedure.reset! @procedure.reset!
end end
end end
def ensure_not_super_admin!
if administrateur_as_manager?
redirect_back fallback_location: root_url, alert: "Interdit aux super admins", status: 403
end
end
private
def administrateur_as_manager?
id = params[:procedure_id] || params[:id]
current_administrateur.administrateurs_procedures
.exists?(procedure_id: id, manager: true)
end
end end
end end

View file

@ -1,6 +1,8 @@
module Administrateurs module Administrateurs
class ArchivesController < AdministrateurController class ArchivesController < AdministrateurController
before_action :retrieve_procedure, only: [:index, :create] before_action :retrieve_procedure
before_action :ensure_not_super_admin!
helper_method :create_archive_url helper_method :create_archive_url
def index def index

View file

@ -1,6 +1,7 @@
module Administrateurs module Administrateurs
class ExportsController < AdministrateurController class ExportsController < AdministrateurController
before_action :retrieve_procedure, only: [:download] before_action :retrieve_procedure
before_action :ensure_not_super_admin!
def download def download
export = Export.find_or_create_export(export_format, all_groupe_instructeurs, **export_options) export = Export.find_or_create_export(export_format, all_groupe_instructeurs, **export_options)

View file

@ -1,6 +1,9 @@
module Administrateurs module Administrateurs
class GroupeInstructeursController < AdministrateurController class GroupeInstructeursController < AdministrateurController
include ActiveSupport::NumberHelper include ActiveSupport::NumberHelper
before_action :ensure_not_super_admin!, only: [:add_instructeur]
ITEMS_PER_PAGE = 25 ITEMS_PER_PAGE = 25
CSV_MAX_SIZE = 1.megabytes CSV_MAX_SIZE = 1.megabytes
CSV_ACCEPTED_CONTENT_TYPES = [ CSV_ACCEPTED_CONTENT_TYPES = [

View file

@ -1,6 +1,7 @@
module Administrateurs module Administrateurs
class ProcedureAdministrateursController < AdministrateurController class ProcedureAdministrateursController < AdministrateurController
before_action :retrieve_procedure, except: [:new] before_action :retrieve_procedure, except: [:new]
before_action :ensure_not_super_admin!, only: [:create]
def index def index
end end

View file

@ -1,6 +1,8 @@
module Instructeurs module Instructeurs
class ArchivesController < InstructeurController class ArchivesController < InstructeurController
before_action :retrieve_procedure, only: [:index, :create] before_action :retrieve_procedure
before_action :ensure_not_super_admin!
helper_method :create_archive_url helper_method :create_archive_url
def index def index

View file

@ -5,5 +5,24 @@ module Instructeurs
def nav_bar_profile def nav_bar_profile
:instructeur :instructeur
end end
def ensure_not_super_admin!
if instructeur_as_manager?
redirect_back fallback_location: root_url, alert: "Interdit aux super admins", status: 403
end
end
private
def instructeur_as_manager?
procedure_id = params[:procedure_id]
current_instructeur.assign_to
.where(instructeur: current_instructeur,
groupe_instructeur: current_instructeur.groupe_instructeurs.where(procedure_id: procedure_id),
manager: true)
.count
.positive?
end
end end
end end

View file

@ -1,6 +1,7 @@
module Instructeurs module Instructeurs
class ProceduresController < InstructeurController class ProceduresController < InstructeurController
before_action :ensure_ownership!, except: [:index] before_action :ensure_ownership!, except: [:index]
before_action :ensure_not_super_admin!, only: [:download_export]
ITEMS_PER_PAGE = 25 ITEMS_PER_PAGE = 25
@ -60,7 +61,7 @@ module Instructeurs
@counts = current_instructeur @counts = current_instructeur
.dossiers_count_summary(groupe_instructeur_ids) .dossiers_count_summary(groupe_instructeur_ids)
.symbolize_keys .symbolize_keys
@can_download_dossiers = (@counts[:tous] + @counts[:archives]) > 0 @can_download_dossiers = (@counts[:tous] + @counts[:archives]) > 0 && !instructeur_as_manager?
dossiers = Dossier.where(groupe_instructeur_id: groupe_instructeur_ids) dossiers = Dossier.where(groupe_instructeur_id: groupe_instructeur_ids)
dossiers_count = @counts[statut.underscore.to_sym] dossiers_count = @counts[statut.underscore.to_sym]
@ -101,7 +102,7 @@ module Instructeurs
@a_suivre_count, @suivis_count, @traites_count, @tous_count, @archives_count, @supprimes_recemment_count, @expirant_count = current_instructeur @a_suivre_count, @suivis_count, @traites_count, @tous_count, @archives_count, @supprimes_recemment_count, @expirant_count = current_instructeur
.dossiers_count_summary(groupe_instructeur_ids) .dossiers_count_summary(groupe_instructeur_ids)
.fetch_values('a_suivre', 'suivis', 'traites', 'tous', 'archives', 'supprimes_recemment', 'expirant') .fetch_values('a_suivre', 'suivis', 'traites', 'tous', 'archives', 'supprimes_recemment', 'expirant')
@can_download_dossiers = (@tous_count + @archives_count) > 0 @can_download_dossiers = (@tous_count + @archives_count) > 0 && !instructeur_as_manager?
notifications = current_instructeur.notifications_for_groupe_instructeurs(groupe_instructeur_ids) notifications = current_instructeur.notifications_for_groupe_instructeurs(groupe_instructeur_ids)
@has_en_cours_notifications = notifications[:en_cours].present? @has_en_cours_notifications = notifications[:en_cours].present?
@ -145,7 +146,7 @@ module Instructeurs
@can_download_dossiers = current_instructeur @can_download_dossiers = current_instructeur
.dossiers .dossiers
.visible_by_administration .visible_by_administration
.exists?(groupe_instructeur_id: groupe_instructeur_ids) .exists?(groupe_instructeur_id: groupe_instructeur_ids) && !instructeur_as_manager?
export = Export.find_or_create_export(export_format, groupe_instructeurs, **export_options) export = Export.find_or_create_export(export_format, groupe_instructeurs, **export_options)

View file

@ -46,23 +46,39 @@ module Manager
send_data(emails.join("\n"), :filename => "brouillons-#{procedure.id}-au-#{date}.csv") send_data(emails.join("\n"), :filename => "brouillons-#{procedure.id}-au-#{date}.csv")
end end
def add_administrateur def add_administrateur_and_instructeur
add_self = params[:email].blank? administrateur = Administrateur.by_email(current_super_admin.email)
administrateur_email = add_self ? current_super_admin.email : params[:email] instructeur = Instructeur.by_email(current_super_admin.email)
administrateur = Administrateur.by_email(administrateur_email) if administrateur && instructeur
if administrateur ActiveRecord::Base.transaction do
AdministrateursProcedure.create(procedure: procedure, administrateur: administrateur, manager: add_self) AdministrateursProcedure.create!(procedure: procedure, administrateur: administrateur, manager: true)
if add_self procedure.groupe_instructeurs.map do |groupe_instructeur|
flash[:notice] = "Ladministrateur \"#{administrateur_email}\" est ajouté à la démarche pour la journée." instructeur.assign_to.create(groupe_instructeur: groupe_instructeur, manager: true)
else end
flash[:notice] = "Ladministrateur \"#{administrateur_email}\" est ajouté à la démarche."
end end
flash[:notice] = "Ladministrateur \"#{administrateur.email}\" a été ajouté à la démarche. L'instructeur \"#{instructeur.email}\" a été ajouté aux #{procedure.groupe_instructeurs.count} groupes d'instructeurs"
else else
if add_self flash[:alert] = "Ladministrateur \"#{administrateur.email}\" est introuvable."
flash[:alert] = "Vous nêtes pas connecté en tant quadministrateur." end
else redirect_to manager_procedure_path(procedure)
flash[:alert] = "Ladministrateur \"#{administrateur_email}\" est introuvable." end
end
def add_administrateur_with_confirmation
confirmation_url = confirm_add_administrateur_manager_procedure_url(id: procedure.id, email: current_super_admin.email)
flash[:notice] = "Veuillez partager ce lien : #{confirmation_url} avec un autre super admin pour que l'operation soit effectuée"
redirect_to manager_procedure_path(procedure)
end
def confirm_add_administrateur
administrateur_email = params[:email]
if administrateur_email != current_super_admin.email
administrateur = Administrateur.by_email(params[:email])
AdministrateursProcedure.create!(procedure: procedure, administrateur: administrateur)
flash[:notice] = "Ladministrateur \"#{administrateur.email}\" a été ajouté à la démarche."
else
flash[:alert] = "Veuillez partager ce lien avec un autre super administrateur pour qu'il confirme votre action"
end end
redirect_to manager_procedure_path(procedure) redirect_to manager_procedure_path(procedure)
end end

View file

@ -4,5 +4,6 @@ class Cron::PurgeManagerAdministrateurSessionsJob < Cron::CronJob
def perform def perform
# TODO: add id column to administrateurs_procedures and use destroy_all # TODO: add id column to administrateurs_procedures and use destroy_all
AdministrateursProcedure.where(manager: true).delete_all AdministrateursProcedure.where(manager: true).delete_all
AssignTo.where(manager: true).destroy_all
end end
end end

View file

@ -15,7 +15,8 @@ class Administrateur < ApplicationRecord
UNUSED_ADMIN_THRESHOLD = 6.months UNUSED_ADMIN_THRESHOLD = 6.months
has_and_belongs_to_many :instructeurs has_and_belongs_to_many :instructeurs
has_and_belongs_to_many :procedures has_many :administrateurs_procedures
has_many :procedures, through: :administrateurs_procedures
has_many :services has_many :services
belongs_to :user belongs_to :user
@ -102,7 +103,7 @@ class Administrateur < ApplicationRecord
# We can't destroy a service if it has procedures, even if those procedures are archived # We can't destroy a service if it has procedures, even if those procedures are archived
service.destroy unless service.procedures.with_discarded.any? service.destroy unless service.procedures.with_discarded.any?
end end
AdministrateursProcedure.where(administrateur_id: self.id).delete_all
destroy destroy
end end

View file

@ -6,6 +6,7 @@
# daily_email_notifications_enabled :boolean default(FALSE), not null # daily_email_notifications_enabled :boolean default(FALSE), not null
# instant_email_dossier_notifications_enabled :boolean default(FALSE), not null # instant_email_dossier_notifications_enabled :boolean default(FALSE), not null
# instant_email_message_notifications_enabled :boolean default(FALSE), not null # instant_email_message_notifications_enabled :boolean default(FALSE), not null
# manager :boolean default(FALSE)
# weekly_email_notifications_enabled :boolean default(TRUE), not null # weekly_email_notifications_enabled :boolean default(TRUE), not null
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime

View file

@ -7,6 +7,7 @@
.container .container
%h1.mb-2 %h1.mb-2
Archives Archives
-# index not renderable as administrateur flagged as manager, so render it anyway
= render Dossiers::ExportComponent.new(procedure: @procedure, exports: @exports, export_url: method(:download_admin_procedure_exports_path)) = render Dossiers::ExportComponent.new(procedure: @procedure, exports: @exports, export_url: method(:download_admin_procedure_exports_path))
= render partial: "shared/archives/notice" = render partial: "shared/archives/notice"

View file

@ -1,3 +1,4 @@
-# not renderable as administrateur flagged as manager, so render it anyway
- if @can_download_dossiers - if @can_download_dossiers
= turbo_stream.update_all '.procedure-actions' do = turbo_stream.update_all '.procedure-actions' do
= render Dossiers::ExportComponent.new(procedure: @procedure, exports: @exports, count: @dossiers_count, export_url: method(:admin_procedure_exports_path)) = render Dossiers::ExportComponent.new(procedure: @procedure, exports: @exports, count: @dossiers_count, export_url: method(:admin_procedure_exports_path))

View file

@ -5,15 +5,20 @@
.instructeur-wrapper .instructeur-wrapper
- if !procedure.routee? - if !procedure.routee?
%p.notice Entrez les adresses email des instructeurs que vous souhaitez affecter à cette démarche %p.notice Entrez les adresses email des instructeurs que vous souhaitez affecter à cette démarche
= hidden_field_tag :emails, nil
= react_component("ComboMultiple",
options: available_instructeur_emails, selected: [], disabled: [],
group: '.instructeur-wrapper',
name: 'emails',
label: 'Emails',
acceptNewValues: true)
= f.submit 'Affecter', class: 'button primary send' - if disabled_as_super_admin
= f.select :emails, available_instructeur_emails, {}, disabled: disabled_as_super_admin, id: 'instructeur_emails'
- else
= hidden_field_tag :emails, nil
= react_component("ComboMultiple",
options: available_instructeur_emails, selected: [], disabled: [],
group: '.instructeur-wrapper',
id: 'instructeur_emails',
name: 'emails',
label: 'Emails',
acceptNewValues: true)
= f.submit 'Affecter', class: 'button primary send', disabled: disabled_as_super_admin
%table.table.mt-2 %table.table.mt-2
%thead %thead

View file

@ -1,6 +1,6 @@
.card .card
.card-title %h2.card-title= t('.title')
= t('.title')
- if !procedure.routee? - if !procedure.routee?
%p.notice= t('.notice_html') %p.notice= t('.notice_html')
@ -9,16 +9,16 @@
= link_to t('.button.routing_disable'), update_routing_enabled_admin_procedure_groupe_instructeurs_path(procedure, routing: :disable), class: 'button primary mt-1', method: 'patch' = link_to t('.button.routing_disable'), update_routing_enabled_admin_procedure_groupe_instructeurs_path(procedure, routing: :disable), class: 'button primary mt-1', method: 'patch'
- else - else
= link_to t('.button.routing_enable'), update_routing_enabled_admin_procedure_groupe_instructeurs_path(procedure, routing: :enable), class: 'button primary mt-1', method: 'patch' = link_to t('.button.routing_enable'), update_routing_enabled_admin_procedure_groupe_instructeurs_path(procedure, routing: :enable), class: 'button primary mt-1', method: 'patch'
.card
%h2.card-title Lautogestion des instructeurs
%p.notice= t('.self_managment_notice_html')
.card-title.mt-4 Lautogestion des instructeurs = form_for procedure,
%p.notice= t('.self_managment_notice_html') method: :patch,
url: update_instructeurs_self_management_enabled_admin_procedure_groupe_instructeurs_path(procedure),
= form_for procedure, html: { class: 'form procedure-form__column--form no-background' } do |f|
method: :patch, %label.toggle-switch
url: update_instructeurs_self_management_enabled_admin_procedure_groupe_instructeurs_path(procedure), = f.check_box :instructeurs_self_management_enabled, class: 'toggle-switch-checkbox', onchange: 'this.form.submit()'
html: { class: 'form procedure-form__column--form no-background' } do |f| %span.toggle-switch-control.round
%label.toggle-switch %span.toggle-switch-label.on
= f.check_box :instructeurs_self_management_enabled, class: 'toggle-switch-checkbox', onchange: 'this.form.submit()' %span.toggle-switch-label.off
%span.toggle-switch-control.round
%span.toggle-switch-label.on
%span.toggle-switch-label.off

View file

@ -10,6 +10,8 @@
'Instructeurs'] } 'Instructeurs'] }
.container.groupe-instructeur .container.groupe-instructeur
%h1 Gérer les instructeurs et les options d'instruction de « #{@procedure.libelle} »
= render partial: 'administrateurs/groupe_instructeurs/routing', locals: { procedure: @procedure } = render partial: 'administrateurs/groupe_instructeurs/routing', locals: { procedure: @procedure }
- if @procedure.routee? - if @procedure.routee?
@ -19,4 +21,5 @@
locals: { procedure: @procedure, locals: { procedure: @procedure,
groupe_instructeur: @procedure.defaut_groupe_instructeur, groupe_instructeur: @procedure.defaut_groupe_instructeur,
instructeurs: @instructeurs, instructeurs: @instructeurs,
available_instructeur_emails: @available_instructeur_emails } available_instructeur_emails: @available_instructeur_emails,
disabled_as_super_admin: administrateur_as_manager? }

View file

@ -1,4 +1,3 @@
= render partial: 'administrateurs/breadcrumbs', = render partial: 'administrateurs/breadcrumbs',
locals: { steps: [link_to('Démarches', admin_procedures_path), locals: { steps: [link_to('Démarches', admin_procedures_path),
link_to(@procedure.libelle, admin_procedure_path(@procedure)), link_to(@procedure.libelle, admin_procedure_path(@procedure)),
@ -11,4 +10,5 @@
locals: { procedure: @procedure, locals: { procedure: @procedure,
groupe_instructeur: @groupe_instructeur, groupe_instructeur: @groupe_instructeur,
instructeurs: @instructeurs, instructeurs: @instructeurs,
available_instructeur_emails: @available_instructeur_emails } available_instructeur_emails: @available_instructeur_emails,
disabled_as_super_admin: administrateur_as_manager? }

View file

@ -1,9 +1,9 @@
= form_for procedure.administrateurs.new(user: User.new), = form_for procedure.administrateurs.new(user: User.new),
url: { controller: 'procedure_administrateurs' }, url: { controller: 'procedure_administrateurs' },
html: { class: 'form', id: "new_administrateur" }, html: { class: 'form', id: "new_administrateur" },
data: { turbo: true } do |f| data: { turbo: true } do |f|
= f.label :email do = f.label :email do
Ajouter un administrateur Ajouter un administrateur
%p.notice Renseignez lemail dun administrateur déjà enregistré sur #{APPLICATION_NAME} pour lui permettre de modifier « #{procedure.libelle} ». %p.notice Renseignez lemail dun administrateur déjà enregistré sur #{APPLICATION_NAME} pour lui permettre de modifier « #{procedure.libelle} ».
= f.email_field :email, placeholder: 'marie.dupont@exemple.fr', required: true = f.email_field :email, placeholder: 'marie.dupont@exemple.fr', required: true, disabled: disabled_as_super_admin
= f.submit 'Ajouter comme administrateur', class: 'button primary send' = f.submit 'Ajouter comme administrateur', class: 'button primary send', disabled: disabled_as_super_admin

View file

@ -1,3 +1,3 @@
- if @administrateur.present? - if @administrateur.present?
= turbo_stream.append "administrateurs", partial: 'administrateur', locals: { procedure: @procedure, administrateur: @administrateur } = turbo_stream.append "administrateurs", partial: 'administrateur', locals: { procedure: @procedure, administrateur: @administrateur }
= turbo_stream.replace "new_administrateur", partial: 'add_admin_form', locals: { procedure: @procedure } = turbo_stream.replace "new_administrateur", partial: 'add_admin_form', locals: { procedure: @procedure, disabled_as_super_admin: administrateur_as_manager? }

View file

@ -4,7 +4,8 @@
'Administrateurs'], preview: false } 'Administrateurs'], preview: false }
.container .container
%h1 Administrateurs de « #{@procedure.libelle} » %h1 Gérer les administrateurs de « #{@procedure.libelle} »
%table.table %table.table
%thead %thead
%th= 'Adresse email' %th= 'Adresse email'
@ -15,4 +16,4 @@
%tfoot %tfoot
%tr %tr
%th{ colspan: 4 } %th{ colspan: 4 }
= render 'add_admin_form', procedure: @procedure = render 'add_admin_form', procedure: @procedure, disabled_as_super_admin: administrateur_as_manager?

View file

@ -1,3 +1,4 @@
-# not renderable as instructeur flagged as manager, so render it anyway
- if @can_download_dossiers - if @can_download_dossiers
- if @statut.nil? - if @statut.nil?
= turbo_stream.update_all '.procedure-actions' do = turbo_stream.update_all '.procedure-actions' do

View file

@ -64,17 +64,19 @@ as well as a link to its edit page.
<dd class="attribute-data attribute-data--<%=attribute.html_class%>"> <dd class="attribute-data attribute-data--<%=attribute.html_class%>">
<%= render_field attribute, page: page %> <%= render_field attribute, page: page %>
<% if attribute.name == 'administrateurs' %> <% if attribute.name == 'administrateurs' %>
<%= form_tag(add_administrateur_manager_procedure_path(procedure), style: 'margin-top: 1rem;') do %> <%= form_tag(add_administrateur_with_confirmation_manager_procedure_path(procedure), style: 'margin-top: 1rem;') do %>
<%= email_field_tag(:email, '', placeholder: 'Email', autocapitalize: 'off', autocorrect: 'off', spellcheck: 'false', style: 'margin-bottom: 1rem;width:24rem;') %> <%= email_field_tag(:email, '', placeholder: 'Email', autocapitalize: 'off', autocorrect: 'off', spellcheck: 'false', style: 'margin-bottom: 1rem;width:24rem;') %>
<button>Ajouter un administrateur</button> <button>Ajouter un administrateur (pour toujours)</button>
<p>J'utilise cette option ETQ support quand un usager a besoin de devenir administrateur sur une démarche</p>
<% end %> <% end %>
<% if procedure.administrateurs.find { |admin| admin.email == current_super_admin.email } %> <% if procedure.administrateurs.find { |admin| admin.email == current_super_admin.email } %>
<p style="margin-top: 20px;">Vous êtes déjà administrateur sur cette démarche</p> <p style="margin-top: 20px;">Vous êtes déjà administrateur sur cette démarche</p>
<%= link_to 'Me retirer de cette démarche', delete_administrateur_manager_procedure_path(procedure), method: :put, class: 'button' %> <%= link_to 'Me retirer de cette démarche', delete_administrateur_manager_procedure_path(procedure), method: :put, class: 'button' %>
<% else %> <% else %>
<%= form_tag(add_administrateur_manager_procedure_path(procedure), style: 'margin-top: 1rem;') do %> <%= form_tag(add_administrateur_and_instructeur_manager_procedure_path(procedure), style: 'margin-top: 1rem;') do %>
<button>Devenir administrateur (pour la journée)</button> <button>Devenir administrateur & instructeur (pour la journée)</button>
<p>J'utilise cette option temporairement à des fins de support.</p>
<% end %> <% end %>
<% end %> <% end %>
<% end %> <% end %>

View file

@ -15,8 +15,10 @@ Rails.application.routes.draw do
post 'draft', on: :member post 'draft', on: :member
post 'discard', on: :member post 'discard', on: :member
post 'restore', on: :member post 'restore', on: :member
post 'add_administrateur', on: :member
put 'delete_administrateur', on: :member put 'delete_administrateur', on: :member
post 'add_administrateur_and_instructeur', on: :member
post 'add_administrateur_with_confirmation', on: :member
get 'confirm_add_administrateur', on: :member
post 'change_piece_justificative_template', on: :member post 'change_piece_justificative_template', on: :member
get 'export_mail_brouillons', on: :member get 'export_mail_brouillons', on: :member
end end

View file

@ -0,0 +1,10 @@
class AddManagerToAssignTos < ActiveRecord::Migration[6.1]
def up
add_column :assign_tos, :manager, :boolean
change_column_default :assign_tos, :manager, false
end
def down
remove_column :assign_tos, :manager
end
end

View file

@ -0,0 +1,10 @@
class BackfillAssignTosManager < ActiveRecord::Migration[6.1]
disable_ddl_transaction!
def change
AssignTo.in_batches do |relation|
relation.update_all manager: false
sleep(0.01)
end
end
end

View file

@ -112,6 +112,7 @@ ActiveRecord::Schema.define(version: 2022_07_28_084804) do
t.boolean "instant_email_dossier_notifications_enabled", default: false, null: false t.boolean "instant_email_dossier_notifications_enabled", default: false, null: false
t.boolean "instant_email_message_notifications_enabled", default: false, null: false t.boolean "instant_email_message_notifications_enabled", default: false, null: false
t.integer "instructeur_id" t.integer "instructeur_id"
t.boolean "manager", default: false
t.datetime "updated_at" t.datetime "updated_at"
t.boolean "weekly_email_notifications_enabled", default: true, null: false t.boolean "weekly_email_notifications_enabled", default: true, null: false
t.index ["groupe_instructeur_id", "instructeur_id"], name: "unique_couple_groupe_instructeur_instructeur", unique: true t.index ["groupe_instructeur_id", "instructeur_id"], name: "unique_couple_groupe_instructeur_instructeur", unique: true

View file

@ -1,6 +1,7 @@
describe Administrateurs::ArchivesController, type: :controller do describe Administrateurs::ArchivesController, type: :controller do
let(:admin) { create(:administrateur) } let(:admin) { create(:administrateur) }
let(:procedure) { create :procedure, administrateur: admin, groupe_instructeurs: [groupe_instructeur1, groupe_instructeur2] } let(:procedure) { create :procedure, groupe_instructeurs: [groupe_instructeur1, groupe_instructeur2] }
let(:administrateur_procedure) { create(:administrateurs_procedure, procedure: procedure, administrateur: admin, manager: manager) }
let(:groupe_instructeur1) { create(:groupe_instructeur) } let(:groupe_instructeur1) { create(:groupe_instructeur) }
let(:groupe_instructeur2) { create(:groupe_instructeur) } let(:groupe_instructeur2) { create(:groupe_instructeur) }
@ -10,8 +11,11 @@ describe Administrateurs::ArchivesController, type: :controller do
context 'when logged out' do context 'when logged out' do
it { is_expected.to have_http_status(302) } it { is_expected.to have_http_status(302) }
end end
context 'when logged in' do
context 'when logged in as administrateur_procedure.manager=false' do
let(:manager) { false }
before do before do
administrateur_procedure
sign_in(admin.user) sign_in(admin.user)
end end
@ -22,15 +26,30 @@ describe Administrateurs::ArchivesController, type: :controller do
subject subject
end end
end end
context 'when logged in as administrateur_procedure.manager=true' do
let(:manager) { true }
before do
administrateur_procedure
sign_in(admin.user)
end
it { is_expected.to have_http_status(403) }
end
end end
describe 'GET #create' do describe 'GET #create' do
subject { post :create, params: { procedure_id: procedure.id, month: '22-06', type: 'monthly' } } subject { post :create, params: { procedure_id: procedure.id, month: '22-06', type: 'monthly' } }
context 'when logged out' do context 'when logged out' do
it { is_expected.to have_http_status(302) } it { is_expected.to have_http_status(302) }
end end
context 'when logged in' do
context 'when logged in in as administrateur_procedure.manager=false' do
let(:manager) { false }
before do before do
administrateur_procedure
sign_in(admin.user) sign_in(admin.user)
end end
@ -39,5 +58,19 @@ describe Administrateurs::ArchivesController, type: :controller do
expect { subject }.to have_enqueued_job(ArchiveCreationJob).with(procedure, an_instance_of(Archive), admin) expect { subject }.to have_enqueued_job(ArchiveCreationJob).with(procedure, an_instance_of(Archive), admin)
end end
end end
context 'when logged in in as administrateur_procedure.manager=true' do
let(:manager) { true }
before do
administrateur_procedure
sign_in(admin.user)
end
it { is_expected.to have_http_status(403) }
it 'does not enqueue the creation job' do
expect { subject }.not_to have_enqueued_job(ArchiveCreationJob)
end
end
end end
end end

View file

@ -63,5 +63,22 @@ describe Administrateurs::ExportsController, type: :controller do
end end
end end
end end
context 'when admin is allowed present as manager' do
let!(:procedure) { create(:procedure) }
let!(:administrateur_procedure) { create(:administrateurs_procedure, procedure: procedure, administrateur: administrateur, manager: true) }
context 'get #index.html' do
it { is_expected.to have_http_status(:forbidden) }
end
context 'get #index.turbo_stream' do
it 'is forbidden' do
post :download,
params: { export_format: :csv, procedure_id: procedure.id },
format: :turbo_stream
expect(response).to have_http_status(:forbidden)
end
end
end
end end
end end

View file

@ -208,10 +208,11 @@ describe Administrateurs::GroupeInstructeursController, type: :controller do
end end
describe '#add_instructeur_procedure_non_routee' do describe '#add_instructeur_procedure_non_routee' do
let(:procedure) { create :procedure, administrateur: admin } let(:procedure) { create :procedure }
let!(:groupe_instructeur) { create(:administrateurs_procedure, procedure: procedure, administrateur: admin, manager: manager) }
let(:emails) { ['instructeur_3@ministere_a.gouv.fr', 'instructeur_4@ministere_b.gouv.fr'].to_json } let(:emails) { ['instructeur_3@ministere_a.gouv.fr', 'instructeur_4@ministere_b.gouv.fr'].to_json }
subject { post :add_instructeur, params: { emails: emails, procedure_id: procedure.id, id: gi_1_1.id } } subject { post :add_instructeur, params: { emails: emails, procedure_id: procedure.id, id: gi_1_1.id } }
let(:manager) { false }
context 'when all emails are valid' do context 'when all emails are valid' do
let(:emails) { ['test@b.gouv.fr', 'test2@b.gouv.fr'].to_json } let(:emails) { ['test@b.gouv.fr', 'test2@b.gouv.fr'].to_json }
it { expect(response.status).to eq(200) } it { expect(response.status).to eq(200) }
@ -233,18 +234,17 @@ describe Administrateurs::GroupeInstructeursController, type: :controller do
it { expect(subject.request.flash[:alert]).to be_present } it { expect(subject.request.flash[:alert]).to be_present }
it { expect(subject).to redirect_to admin_procedure_groupe_instructeurs_path(procedure) } it { expect(subject).to redirect_to admin_procedure_groupe_instructeurs_path(procedure) }
end end
context 'when signed in admin comes from manager' do
let(:manager) { true }
it { is_expected.to have_http_status(:forbidden) }
end
end end
describe '#add_instructeur' do describe '#add_instructeur' do
let!(:instructeur) { create(:instructeur) } let!(:instructeur) { create(:instructeur) }
let(:gi_1_2) { procedure.groupe_instructeurs.create(label: 'groupe instructeur 2') } let(:gi_1_2) { procedure.groupe_instructeurs.create(label: 'groupe instructeur 2') }
let(:do_request) do
before do
gi_1_2.instructeurs << instructeur
allow(GroupeInstructeurMailer).to receive(:add_instructeurs)
.and_return(double(deliver_later: true))
post :add_instructeur, post :add_instructeur,
params: { params: {
procedure_id: procedure.id, procedure_id: procedure.id,
@ -252,10 +252,16 @@ describe Administrateurs::GroupeInstructeursController, type: :controller do
emails: new_instructeur_emails.to_json emails: new_instructeur_emails.to_json
} }
end end
before do
gi_1_2.instructeurs << instructeur
allow(GroupeInstructeurMailer).to receive(:add_instructeurs)
.and_return(double(deliver_later: true))
end
context 'of a news instructeurs' do context 'of a news instructeurs' do
let(:new_instructeur_emails) { ['new_i1@mail.com', 'new_i2@mail.com'] } let(:new_instructeur_emails) { ['new_i1@mail.com', 'new_i2@mail.com'] }
before { do_request }
it { expect(gi_1_2.instructeurs.pluck(:email)).to include(*new_instructeur_emails) } it { expect(gi_1_2.instructeurs.pluck(:email)).to include(*new_instructeur_emails) }
it { expect(flash.notice).to be_present } it { expect(flash.notice).to be_present }
it { expect(response).to redirect_to(admin_procedure_groupe_instructeur_path(procedure, gi_1_2)) } it { expect(response).to redirect_to(admin_procedure_groupe_instructeur_path(procedure, gi_1_2)) }
@ -271,22 +277,32 @@ describe Administrateurs::GroupeInstructeursController, type: :controller do
context 'of an instructeur already in the group' do context 'of an instructeur already in the group' do
let(:new_instructeur_emails) { [instructeur.email] } let(:new_instructeur_emails) { [instructeur.email] }
before { do_request }
it { expect(response).to redirect_to(admin_procedure_groupe_instructeur_path(procedure, gi_1_2)) } it { expect(response).to redirect_to(admin_procedure_groupe_instructeur_path(procedure, gi_1_2)) }
end end
context 'of badly formed email' do context 'of badly formed email' do
let(:new_instructeur_emails) { ['badly_formed_email'] } let(:new_instructeur_emails) { ['badly_formed_email'] }
before { do_request }
it { expect(flash.alert).to be_present } it { expect(flash.alert).to be_present }
it { expect(response).to redirect_to(admin_procedure_groupe_instructeur_path(procedure, gi_1_2)) } it { expect(response).to redirect_to(admin_procedure_groupe_instructeur_path(procedure, gi_1_2)) }
end end
context 'of an empty string' do context 'of an empty string' do
let(:new_instructeur_emails) { [''] } let(:new_instructeur_emails) { [''] }
before { do_request }
it { expect(response).to redirect_to(admin_procedure_groupe_instructeur_path(procedure, gi_1_2)) } it { expect(response).to redirect_to(admin_procedure_groupe_instructeur_path(procedure, gi_1_2)) }
end end
context 'when connected as an administrateur from manager' do
let(:new_instructeur_emails) { [instructeur.email] }
before do
admin.administrateurs_procedures.update_all(manager: true)
do_request
end
it { expect(response).to have_http_status(:forbidden) }
end
end end
describe '#remove_instructeur' do describe '#remove_instructeur' do

View file

@ -1,14 +1,24 @@
describe Administrateurs::ProcedureAdministrateursController, type: :controller do describe Administrateurs::ProcedureAdministrateursController, type: :controller do
let(:signed_in_admin) { create(:administrateur) } let(:signed_in_admin) { create(:administrateur) }
let(:other_admin) { create(:administrateur) } let(:other_admin) { create(:administrateur) }
let(:procedure) { create(:procedure, administrateurs: [signed_in_admin, other_admin]) } let!(:administrateurs_procedure) { create(:administrateurs_procedure, administrateur: signed_in_admin, procedure: procedure, manager: manager) }
let!(:procedure) { create(:procedure, administrateurs: [other_admin]) }
render_views render_views
before do before do
sign_in(signed_in_admin.user) sign_in(signed_in_admin.user)
end end
describe '#create' do
context 'as manager' do
let(:manager) { true }
subject { post :create, params: { procedure_id: procedure.id, administrateur: { email: create(:administrateur).email } }, format: :turbo_stream }
it { is_expected.to have_http_status(:forbidden) }
end
end
describe '#destroy' do describe '#destroy' do
let(:manager) { false }
subject do subject do
delete :destroy, params: { procedure_id: procedure.id, id: admin_to_remove.id }, format: :turbo_stream delete :destroy, params: { procedure_id: procedure.id, id: admin_to_remove.id }, format: :turbo_stream
end end

View file

@ -1,16 +1,15 @@
describe Instructeurs::ArchivesController, type: :controller do describe Instructeurs::ArchivesController, type: :controller do
let(:procedure1) { create(:procedure, :published, groupe_instructeurs: [gi1]) } let(:procedure1) { create(:procedure, :published, groupe_instructeurs: [assign_to.groupe_instructeur]) }
let(:procedure2) { create(:procedure, :published, groupe_instructeurs: [gi2]) } let(:procedure2) { create(:procedure, :published, groupe_instructeurs: [gi2]) }
let!(:instructeur) { create(:instructeur, groupe_instructeurs: [gi1, gi2]) } let!(:instructeur) { create(:instructeur, groupe_instructeurs: [gi2]) }
let!(:archive1) { create(:archive, :generated, groupe_instructeurs: [gi1]) } let!(:archive1) { create(:archive, :generated, groupe_instructeurs: [assign_to.groupe_instructeur]) }
let!(:archive2) { create(:archive, :generated, groupe_instructeurs: [gi2]) } let!(:archive2) { create(:archive, :generated, groupe_instructeurs: [gi2]) }
let(:gi1) { create(:groupe_instructeur) } let!(:assign_to) { create(:assign_to, instructeur: instructeur, groupe_instructeur: build(:groupe_instructeur), manager: manager) }
let(:gi2) { create(:groupe_instructeur) } let(:gi2) { create(:groupe_instructeur) }
before do before do
sign_in(instructeur.user) sign_in(instructeur.user)
end end
after { Timecop.return } after { Timecop.return }
describe '#index' do describe '#index' do
@ -20,24 +19,50 @@ describe Instructeurs::ArchivesController, type: :controller do
create_dossier_for_month(procedure1, 2021, 2) create_dossier_for_month(procedure1, 2021, 2)
Timecop.freeze(Time.zone.local(2021, 3, 5)) Timecop.freeze(Time.zone.local(2021, 3, 5))
end end
subject { get :index, params: { procedure_id: procedure1.id } }
it 'displays archives' do context 'signed in not as manager' do
get :index, params: { procedure_id: procedure1.id } let(:manager) { false }
expect(assigns(:archives)).to eq([archive1]) it { is_expected.to have_http_status(:success) }
it 'assigns archives' do
subject
expect(assigns(:archives)).to eq([archive1])
end
end
context 'signed in as manager' do
let(:manager) { true }
before do
sign_in(instructeur.user)
end
it { is_expected.to have_http_status(:forbidden) }
end end
end end
describe '#create' do describe '#create' do
let(:month) { '21-03' }
let(:date_month) { Date.strptime(month, "%Y-%m") }
let(:subject) do let(:subject) do
post :create, params: { procedure_id: procedure1.id, type: 'monthly', month: month } post :create, params: { procedure_id: procedure1.id, type: 'monthly', month: month }
end end
it "performs archive creation job" do let(:month) { '21-03' }
expect { subject }.to have_enqueued_job(ArchiveCreationJob).with(procedure1, an_instance_of(Archive), instructeur) let(:date_month) { Date.strptime(month, "%Y-%m") }
expect(flash.notice).to include("Votre demande a été prise en compte")
context 'signed in not as manager' do
let(:manager) { false }
it "performs archive creation job" do
expect { subject }.to have_enqueued_job(ArchiveCreationJob).with(procedure1, an_instance_of(Archive), instructeur)
expect(flash.notice).to include("Votre demande a été prise en compte")
end
end
context 'signed in as manager' do
let(:manager) { true }
it { is_expected.to have_http_status(:forbidden) }
end end
end end

View file

@ -468,9 +468,10 @@ describe Instructeurs::ProceduresController, type: :controller do
describe '#download_export' do describe '#download_export' do
let(:instructeur) { create(:instructeur) } let(:instructeur) { create(:instructeur) }
let!(:procedure) { create(:procedure) } let!(:procedure) { create(:procedure) }
let!(:gi_0) { procedure.defaut_groupe_instructeur } let!(:assign_to) { create(:assign_to, instructeur: instructeur, groupe_instructeur: build(:groupe_instructeur, procedure: procedure), manager: manager) }
let!(:gi_0) { assign_to.groupe_instructeur }
let!(:gi_1) { create(:groupe_instructeur, label: 'gi_1', procedure: procedure, instructeurs: [instructeur]) } let!(:gi_1) { create(:groupe_instructeur, label: 'gi_1', procedure: procedure, instructeurs: [instructeur]) }
let(:manager) { false }
before { sign_in(instructeur.user) } before { sign_in(instructeur.user) }
subject do subject do
@ -498,7 +499,7 @@ describe Instructeurs::ProceduresController, type: :controller do
end end
context 'when the export is ready' do context 'when the export is ready' do
let(:export) { create(:export, groupe_instructeurs: [gi_1], job_status: 'generated') } let(:export) { create(:export, groupe_instructeurs: [gi_1, gi_0], job_status: 'generated') }
before do before do
export.file.attach(io: StringIO.new('export'), filename: 'file.csv') export.file.attach(io: StringIO.new('export'), filename: 'file.csv')
@ -511,7 +512,7 @@ describe Instructeurs::ProceduresController, type: :controller do
end end
context 'when another export is ready' do context 'when another export is ready' do
let(:export) { create(:export, groupe_instructeurs: [gi_0, gi_1]) } let(:export) { create(:export, groupe_instructeurs: [gi_0]) }
before do before do
export.file.attach(io: StringIO.new('export'), filename: 'file.csv') export.file.attach(io: StringIO.new('export'), filename: 'file.csv')
@ -535,6 +536,11 @@ describe Instructeurs::ProceduresController, type: :controller do
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
end end
end end
context 'when logged in through super admin' do
let(:manager) { true }
it { is_expected.to have_http_status(:forbidden) }
end
end end
describe '#create_multiple_commentaire' do describe '#create_multiple_commentaire' do

View file

@ -0,0 +1,6 @@
FactoryBot.define do
factory :administrateurs_procedure do
association :administrateur
association :procedure
end
end

View file

@ -3,7 +3,6 @@ describe Administrateur, type: :model do
describe 'associations' do describe 'associations' do
it { is_expected.to have_and_belong_to_many(:instructeurs) } it { is_expected.to have_and_belong_to_many(:instructeurs) }
it { is_expected.to have_and_belong_to_many(:procedures) }
end end
describe "#renew_api_token" do describe "#renew_api_token" do

View file

@ -0,0 +1,49 @@
require 'system/administrateurs/procedure_spec_helper'
describe 'Administrateurs can manage administrateurs', js: true do
include ProcedureSpecHelper
let(:administrateur) { create(:administrateur) }
let!(:procedure) { create(:procedure) }
let!(:administrateurs_procedure) { create(:administrateurs_procedure, administrateur: administrateur, procedure: procedure, manager: manager) }
let(:manager) { false }
before do
login_as administrateur.user, scope: :user
end
scenario 'card is clickable' do
visit admin_procedure_path(procedure)
find('#administrateurs').click
expect(page).to have_css("h1", text: "Gérer les administrateurs de « #{procedure.libelle} »")
end
context 'as admin not flagged from manager' do
let(:manager) { false }
scenario 'the administrator can add another administrator' do
another_administrateur = create(:administrateur)
visit admin_procedure_administrateurs_path(procedure)
find('#administrateurs').click
fill_in('administrateur_email', with: another_administrateur.email)
click_on 'Ajouter comme administrateur'
within('.alert-success') do
expect(page).to have_content(another_administrateur.email)
end
end
end
context 'as admin flagged from manager' do
let(:manager) { true }
scenario 'the administrator from manager can not add another administrator' do
administrateur.administrateurs_procedures.update_all(manager: true)
visit admin_procedure_administrateurs_path(procedure)
find('#administrateurs').click
expect(page).to have_css("#administrateur_email[disabled=\"disabled\"]")
end
end
end

View file

@ -0,0 +1,46 @@
require 'system/administrateurs/procedure_spec_helper'
describe 'Manage procedure instructeurs', js: true do
include ProcedureSpecHelper
let(:administrateur) { create(:administrateur) }
let!(:procedure) { create(:procedure) }
let!(:administrateurs_procedure) { create(:administrateurs_procedure, administrateur: administrateur, procedure: procedure, manager: manager) }
let(:manager) { false }
before do
login_as administrateur.user, scope: :user
end
context 'is accessible via card' do
let(:manager) { false }
scenario 'it works' do
visit admin_procedure_path(procedure)
find('#groupe-instructeurs').click
expect(page).to have_css("h1", text: "Gérer les instructeurs et les options d'instruction de « #{procedure.libelle} »")
end
end
context 'as admin not from manager' do
let(:manager) { false }
scenario 'can add instructeur' do
visit admin_procedure_groupe_instructeurs_path(procedure)
expect {
fill_in "instructeur_emails", with: create(:instructeur).email
click_on "Affecter"
}.to change { procedure.instructeurs.count }.by(1)
end
end
context 'as admin from manager' do
let(:manager) { true }
scenario 'cannot add instructeur' do
visit admin_procedure_groupe_instructeurs_path(procedure)
expect(page).to have_css("#instructeur_emails[disabled=\"disabled\"]")
end
end
end

View file

@ -58,18 +58,4 @@ describe 'Administrateurs can edit procedures', js: true do
expect(page).to have_selector('.breadcrumbs li', text: 'Ma petite démarche') expect(page).to have_selector('.breadcrumbs li', text: 'Ma petite démarche')
end end
end end
scenario 'the administrator can add another administrator' do
another_administrateur = create(:administrateur)
visit admin_procedure_path(procedure)
find('#administrateurs').click
fill_in('administrateur_email', with: another_administrateur.email)
click_on 'Ajouter comme administrateur'
within('.alert-success') do
expect(page).to have_content(another_administrateur.email)
end
end
end end