Evite de cacher la whitelist trop longtemps

This commit is contained in:
Frederic Merizen 2018-10-04 16:41:09 +02:00
parent af4a50d8a2
commit eafd0e8348
6 changed files with 171 additions and 166 deletions

View file

@ -67,10 +67,11 @@ module NewGestionnaire
@archived_dossiers @archived_dossiers
end end
sorted_ids = DossierFieldService.sorted_ids(@dossiers, procedure_presentation, current_gestionnaire) dossier_field_service = DossierFieldService.new
sorted_ids = dossier_field_service.sorted_ids(@dossiers, procedure_presentation, current_gestionnaire)
if @current_filters.count > 0 if @current_filters.count > 0
filtered_ids = DossierFieldService.filtered_ids(@dossiers, current_filters) filtered_ids = dossier_field_service.filtered_ids(@dossiers, current_filters)
filtered_sorted_ids = sorted_ids.select { |id| filtered_ids.include?(id) } filtered_sorted_ids = sorted_ids.select { |id| filtered_ids.include?(id) }
else else
filtered_sorted_ids = sorted_ids filtered_sorted_ids = sorted_ids

View file

@ -224,7 +224,7 @@ class Dossier < ApplicationRecord
end end
def get_value(table, column) def get_value(table, column)
DossierFieldService.get_value(self, table, column) DossierFieldService.new.get_value(self, table, column)
end end
def owner_name def owner_name

View file

@ -293,7 +293,7 @@ class Procedure < ApplicationRecord
end end
def fields def fields
DossierFieldService.fields(self) DossierFieldService.new.fields(self)
end end
def fields_for_select def fields_for_select

View file

@ -16,7 +16,7 @@ class ProcedurePresentation < ApplicationRecord
displayed_fields.each do |field| displayed_fields.each do |field|
table = field['table'] table = field['table']
column = field['column'] column = field['column']
if !DossierFieldService.valid_column?(procedure, table, column) if !dossier_field_service.valid_column?(procedure, table, column)
errors.add(:filters, "#{table}.#{column} nest pas une colonne permise") errors.add(:filters, "#{table}.#{column} nest pas une colonne permise")
end end
end end
@ -35,7 +35,7 @@ class ProcedurePresentation < ApplicationRecord
columns.each do |column| columns.each do |column|
table = column['table'] table = column['table']
column = column['column'] column = column['column']
if !DossierFieldService.valid_column?(procedure, table, column) if !dossier_field_service.valid_column?(procedure, table, column)
errors.add(:filters, "#{table}.#{column} nest pas une colonne permise") errors.add(:filters, "#{table}.#{column} nest pas une colonne permise")
end end
end end
@ -44,7 +44,11 @@ class ProcedurePresentation < ApplicationRecord
private private
def dossier_field_service
@dossier_field_service ||= DossierFieldService.new
end
def valid_sort_column?(procedure, table, column) def valid_sort_column?(procedure, table, column)
DossierFieldService.valid_column?(procedure, table, column) || EXTRA_SORT_COLUMNS[table]&.include?(column) dossier_field_service.valid_column?(procedure, table, column) || EXTRA_SORT_COLUMNS[table]&.include?(column)
end end
end end

View file

@ -1,172 +1,172 @@
class DossierFieldService class DossierFieldService
@@column_whitelist = {} def initialize
@column_whitelist = {}
end
class << self def fields(procedure)
def fields(procedure) fields = [
fields = [ field_hash('Créé le', 'self', 'created_at'),
field_hash('Créé le', 'self', 'created_at'), field_hash('Mis à jour le', 'self', 'updated_at'),
field_hash('Mis à jour le', 'self', 'updated_at'), field_hash('Demandeur', 'user', 'email')
field_hash('Demandeur', 'user', 'email') ]
]
if !procedure.for_individual || (procedure.for_individual && procedure.individual_with_siret) if !procedure.for_individual || (procedure.for_individual && procedure.individual_with_siret)
fields.push( fields.push(
field_hash('SIREN', 'etablissement', 'entreprise_siren'), field_hash('SIREN', 'etablissement', 'entreprise_siren'),
field_hash('Forme juridique', 'etablissement', 'entreprise_forme_juridique'), field_hash('Forme juridique', 'etablissement', 'entreprise_forme_juridique'),
field_hash('Nom commercial', 'etablissement', 'entreprise_nom_commercial'), field_hash('Nom commercial', 'etablissement', 'entreprise_nom_commercial'),
field_hash('Raison sociale', 'etablissement', 'entreprise_raison_sociale'), field_hash('Raison sociale', 'etablissement', 'entreprise_raison_sociale'),
field_hash('SIRET siège social', 'etablissement', 'entreprise_siret_siege_social'), field_hash('SIRET siège social', 'etablissement', 'entreprise_siret_siege_social'),
field_hash('Date de création', 'etablissement', 'entreprise_date_creation') field_hash('Date de création', 'etablissement', 'entreprise_date_creation')
) )
fields.push( fields.push(
field_hash('SIRET', 'etablissement', 'siret'), field_hash('SIRET', 'etablissement', 'siret'),
field_hash('Libellé NAF', 'etablissement', 'libelle_naf'), field_hash('Libellé NAF', 'etablissement', 'libelle_naf'),
field_hash('Code postal', 'etablissement', 'code_postal') field_hash('Code postal', 'etablissement', 'code_postal')
) )
end
explanatory_types_de_champ = [:header_section, :explication].map{ |k| TypeDeChamp.type_champs.fetch(k) }
fields.concat procedure.types_de_champ
.reject { |tdc| explanatory_types_de_champ.include?(tdc.type_champ) }
.map { |type_de_champ| field_hash(type_de_champ.libelle, 'type_de_champ', type_de_champ.id.to_s) }
fields.concat procedure.types_de_champ_private
.reject { |tdc| explanatory_types_de_champ.include?(tdc.type_champ) }
.map { |type_de_champ| field_hash(type_de_champ.libelle, 'type_de_champ_private', type_de_champ.id.to_s) }
fields
end end
def get_value(dossier, table, column) explanatory_types_de_champ = [:header_section, :explication].map{ |k| TypeDeChamp.type_champs.fetch(k) }
assert_valid_column(dossier.procedure, table, column)
fields.concat procedure.types_de_champ
.reject { |tdc| explanatory_types_de_champ.include?(tdc.type_champ) }
.map { |type_de_champ| field_hash(type_de_champ.libelle, 'type_de_champ', type_de_champ.id.to_s) }
fields.concat procedure.types_de_champ_private
.reject { |tdc| explanatory_types_de_champ.include?(tdc.type_champ) }
.map { |type_de_champ| field_hash(type_de_champ.libelle, 'type_de_champ_private', type_de_champ.id.to_s) }
fields
end
def get_value(dossier, table, column)
assert_valid_column(dossier.procedure, table, column)
case table
when 'self'
dossier.send(column)
when 'user'
dossier.user.send(column)
when 'etablissement'
dossier.etablissement&.send(column)
when 'type_de_champ'
dossier.champs.find { |c| c.type_de_champ_id == column.to_i }.value
when 'type_de_champ_private'
dossier.champs_private.find { |c| c.type_de_champ_id == column.to_i }.value
end
end
def assert_valid_column(procedure, table, column)
if !valid_column?(procedure, table, column)
raise "Invalid column #{table}.#{column}"
end
end
def valid_column?(procedure, table, column)
valid_columns_for_table(procedure, table).include?(column)
end
def filtered_ids(dossiers, filters)
filters.map do |filter|
table = filter['table']
column = sanitized_column(filter)
case table case table
when 'self' when 'self'
dossier.send(column) dossiers.where("? ILIKE ?", filter['column'], "%#{filter['value']}%")
when 'user'
dossier.user.send(column) when 'type_de_champ', 'type_de_champ_private'
relation = table == 'type_de_champ' ? :champs : :champs_private
dossiers
.includes(relation)
.where("champs.type_de_champ_id = ?", filter['column'].to_i)
.where("champs.value ILIKE ?", "%#{filter['value']}%")
when 'etablissement' when 'etablissement'
dossier.etablissement&.send(column) if filter['column'] == 'entreprise_date_creation'
when 'type_de_champ' date = filter['value'].to_date rescue nil
dossier.champs.find { |c| c.type_de_champ_id == column.to_i }.value
when 'type_de_champ_private'
dossier.champs_private.find { |c| c.type_de_champ_id == column.to_i }.value
end
end
def assert_valid_column(procedure, table, column)
if !valid_column?(procedure, table, column)
raise "Invalid column #{table}.#{column}"
end
end
def valid_column?(procedure, table, column)
valid_columns_for_table(procedure, table).include?(column)
end
def filtered_ids(dossiers, filters)
filters.map do |filter|
table = filter['table']
column = sanitized_column(filter)
case table
when 'self'
dossiers.where("? ILIKE ?", filter['column'], "%#{filter['value']}%")
when 'type_de_champ', 'type_de_champ_private'
relation = table == 'type_de_champ' ? :champs : :champs_private
dossiers dossiers
.includes(relation) .includes(table)
.where("champs.type_de_champ_id = ?", filter['column'].to_i) .where("#{column} = ?", date)
.where("champs.value ILIKE ?", "%#{filter['value']}%") else
when 'etablissement'
if filter['column'] == 'entreprise_date_creation'
date = filter['value'].to_date rescue nil
dossiers
.includes(table)
.where("#{column} = ?", date)
else
dossiers
.includes(table)
.where("#{column} ILIKE ?", "%#{filter['value']}%")
end
when 'user'
dossiers dossiers
.includes(table) .includes(table)
.where("#{column} ILIKE ?", "%#{filter['value']}%") .where("#{column} ILIKE ?", "%#{filter['value']}%")
end.pluck(:id)
end.reduce(:&)
end
def sorted_ids(dossiers, procedure_presentation, gestionnaire)
table = procedure_presentation.sort['table']
column = sanitized_column(procedure_presentation.sort)
order = procedure_presentation.sort['order']
assert_valid_order(order)
case table
when 'notifications'
procedure = procedure_presentation.assign_to.procedure
dossiers_id_with_notification = gestionnaire.notifications_for_procedure(procedure)
if order == 'desc'
return dossiers_id_with_notification +
(dossiers.order('dossiers.updated_at desc').ids - dossiers_id_with_notification)
else
return (dossiers.order('dossiers.updated_at asc').ids - dossiers_id_with_notification) +
dossiers_id_with_notification
end end
when 'self' when 'user'
return dossiers dossiers
.order("#{column} #{order}") .includes(table)
.pluck(:id) .where("#{column} ILIKE ?", "%#{filter['value']}%")
when 'type_de_champ', 'type_de_champ_private' end.pluck(:id)
return dossiers end.reduce(:&)
.includes(table == 'type_de_champ' ? :champs : :champs_private) end
.where("champs.type_de_champ_id = #{procedure_presentation.sort['column'].to_i}")
.order("champs.value #{order}") def sorted_ids(dossiers, procedure_presentation, gestionnaire)
.pluck(:id) table = procedure_presentation.sort['table']
column = sanitized_column(procedure_presentation.sort)
order = procedure_presentation.sort['order']
assert_valid_order(order)
case table
when 'notifications'
procedure = procedure_presentation.assign_to.procedure
dossiers_id_with_notification = gestionnaire.notifications_for_procedure(procedure)
if order == 'desc'
return dossiers_id_with_notification +
(dossiers.order('dossiers.updated_at desc').ids - dossiers_id_with_notification)
else else
return dossiers return (dossiers.order('dossiers.updated_at asc').ids - dossiers_id_with_notification) +
.includes(table) dossiers_id_with_notification
.order("#{column} #{order}")
.pluck(:id)
end end
end when 'self'
return dossiers
private .order("#{column} #{order}")
.pluck(:id)
def valid_columns_for_table(procedure, table) when 'type_de_champ', 'type_de_champ_private'
if !@@column_whitelist.key?(procedure.id) return dossiers
@@column_whitelist[procedure.id] = fields(procedure) .includes(table == 'type_de_champ' ? :champs : :champs_private)
.group_by { |field| field['table'] } .where("champs.type_de_champ_id = #{procedure_presentation.sort['column'].to_i}")
.map { |table, fields| [table, Set.new(fields.map { |field| field['column'] }) ] } .order("champs.value #{order}")
.to_h .pluck(:id)
end else
return dossiers
@@column_whitelist[procedure.id][table] || [] .includes(table)
end .order("#{column} #{order}")
.pluck(:id)
def sanitized_column(field)
table = field['table']
table = ActiveRecord::Base.connection.quote_column_name((table == 'self' ? 'dossier' : table).pluralize)
column = ActiveRecord::Base.connection.quote_column_name(field['column'])
table + '.' + column
end
def assert_valid_order(order)
if !["asc", "desc"].include?(order)
raise "Invalid order #{order}"
end
end
def field_hash(label, table, column)
{
'label' => label,
'table' => table,
'column' => column
}
end end
end end
private
def valid_columns_for_table(procedure, table)
if !@column_whitelist.key?(procedure.id)
@column_whitelist[procedure.id] = fields(procedure)
.group_by { |field| field['table'] }
.map { |table, fields| [table, Set.new(fields.map { |field| field['column'] }) ] }
.to_h
end
@column_whitelist[procedure.id][table] || []
end
def sanitized_column(field)
table = field['table']
table = ActiveRecord::Base.connection.quote_column_name((table == 'self' ? 'dossier' : table).pluralize)
column = ActiveRecord::Base.connection.quote_column_name(field['column'])
table + '.' + column
end
def assert_valid_order(order)
if !["asc", "desc"].include?(order)
raise "Invalid order #{order}"
end
end
def field_hash(label, table, column)
{
'label' => label,
'table' => table,
'column' => column
}
end
end end

View file

@ -14,7 +14,7 @@ describe DossierFieldService do
type_de_champ.champ.create(dossier: discarded_dossier, value: 'discard me') type_de_champ.champ.create(dossier: discarded_dossier, value: 'discard me')
end end
subject { described_class.filtered_ids(procedure.dossiers, [{ 'table' => 'type_de_champ', 'column' => type_de_champ.id, 'value' => 'keep' }]) } subject { described_class.new.filtered_ids(procedure.dossiers, [{ 'table' => 'type_de_champ', 'column' => type_de_champ.id, 'value' => 'keep' }]) }
it { is_expected.to contain_exactly(kept_dossier.id) } it { is_expected.to contain_exactly(kept_dossier.id) }
end end
@ -29,7 +29,7 @@ describe DossierFieldService do
type_de_champ_private.champ.create(dossier: discarded_dossier, value: 'discard me') type_de_champ_private.champ.create(dossier: discarded_dossier, value: 'discard me')
end end
subject { described_class.filtered_ids(procedure.dossiers, [{ 'table' => 'type_de_champ_private', 'column' => type_de_champ_private.id, 'value' => 'keep' }]) } subject { described_class.new.filtered_ids(procedure.dossiers, [{ 'table' => 'type_de_champ_private', 'column' => type_de_champ_private.id, 'value' => 'keep' }]) }
it { is_expected.to contain_exactly(kept_dossier.id) } it { is_expected.to contain_exactly(kept_dossier.id) }
end end
@ -39,7 +39,7 @@ describe DossierFieldService do
let!(:kept_dossier) { create(:dossier, procedure: procedure, etablissement: create(:etablissement, entreprise_date_creation: DateTime.new(2018, 6, 21))) } let!(:kept_dossier) { create(:dossier, procedure: procedure, etablissement: create(:etablissement, entreprise_date_creation: DateTime.new(2018, 6, 21))) }
let!(:discarded_dossier) { create(:dossier, procedure: procedure, etablissement: create(:etablissement, entreprise_date_creation: DateTime.new(2008, 6, 21))) } let!(:discarded_dossier) { create(:dossier, procedure: procedure, etablissement: create(:etablissement, entreprise_date_creation: DateTime.new(2008, 6, 21))) }
subject { described_class.filtered_ids(procedure.dossiers, [{ 'table' => 'etablissement', 'column' => 'entreprise_date_creation', 'value' => '21/6/2018' }]) } subject { described_class.new.filtered_ids(procedure.dossiers, [{ 'table' => 'etablissement', 'column' => 'entreprise_date_creation', 'value' => '21/6/2018' }]) }
it { is_expected.to contain_exactly(kept_dossier.id) } it { is_expected.to contain_exactly(kept_dossier.id) }
end end
@ -50,7 +50,7 @@ describe DossierFieldService do
let!(:kept_dossier) { create(:dossier, procedure: procedure, etablissement: create(:etablissement, code_postal: '75017')) } let!(:kept_dossier) { create(:dossier, procedure: procedure, etablissement: create(:etablissement, code_postal: '75017')) }
let!(:discarded_dossier) { create(:dossier, procedure: procedure, etablissement: create(:etablissement, code_postal: '25000')) } let!(:discarded_dossier) { create(:dossier, procedure: procedure, etablissement: create(:etablissement, code_postal: '25000')) }
subject { described_class.filtered_ids(procedure.dossiers, [{ 'table' => 'etablissement', 'column' => 'code_postal', 'value' => '75017' }]) } subject { described_class.new.filtered_ids(procedure.dossiers, [{ 'table' => 'etablissement', 'column' => 'code_postal', 'value' => '75017' }]) }
it { is_expected.to contain_exactly(kept_dossier.id) } it { is_expected.to contain_exactly(kept_dossier.id) }
end end
@ -60,7 +60,7 @@ describe DossierFieldService do
let!(:kept_dossier) { create(:dossier, procedure: procedure, user: create(:user, email: 'me@keepmail.com')) } let!(:kept_dossier) { create(:dossier, procedure: procedure, user: create(:user, email: 'me@keepmail.com')) }
let!(:discarded_dossier) { create(:dossier, procedure: procedure, user: create(:user, email: 'me@discard.com')) } let!(:discarded_dossier) { create(:dossier, procedure: procedure, user: create(:user, email: 'me@discard.com')) }
subject { described_class.filtered_ids(procedure.dossiers, [{ 'table' => 'user', 'column' => 'email', 'value' => 'keepmail' }]) } subject { described_class.new.filtered_ids(procedure.dossiers, [{ 'table' => 'user', 'column' => 'email', 'value' => 'keepmail' }]) }
it { is_expected.to contain_exactly(kept_dossier.id) } it { is_expected.to contain_exactly(kept_dossier.id) }
end end
@ -72,7 +72,7 @@ describe DossierFieldService do
let(:sort) { { 'table' => table, 'column' => column, 'order' => order } } let(:sort) { { 'table' => table, 'column' => column, 'order' => order } }
let(:procedure_presentation) { ProcedurePresentation.create(assign_to: assign_to, sort: sort) } let(:procedure_presentation) { ProcedurePresentation.create(assign_to: assign_to, sort: sort) }
subject { DossierFieldService.sorted_ids(procedure.dossiers, procedure_presentation, gestionnaire) } subject { described_class.new.sorted_ids(procedure.dossiers, procedure_presentation, gestionnaire) }
context 'for notifications table' do context 'for notifications table' do
let(:table) { 'notifications' } let(:table) { 'notifications' }
@ -164,7 +164,7 @@ describe DossierFieldService do
end end
describe '#get_value' do describe '#get_value' do
subject { DossierFieldService.get_value(dossier, table, column) } subject { described_class.new.get_value(dossier, table, column) }
context 'for self table' do context 'for self table' do
let(:table) { 'self' } let(:table) { 'self' }