DEV: Set limits on custom fields

This patch sets some limits on custom fields:
- an entity can’t have more than 100 custom fields defined on it
- a custom field can’t hold a value greater than 10,000,000 characters

The current implementation of custom fields is relatively complex and
does an upsert in SQL at some point, thus preventing to simply add an
`ActiveRecord` validation on the custom field model without having to
rewrite a part of the existing logic.
That’s one of the reasons this patch is implementing validations in the
`HasCustomField` module adding them to the model including the module.
This commit is contained in:
Loïc Guitaut 2023-06-05 17:38:50 +02:00 committed by Loïc Guitaut
parent c08a52e502
commit 5257c80064
8 changed files with 74 additions and 4 deletions

View File

@ -60,12 +60,19 @@ module HasCustomFields
end
end
included do
has_many :_custom_fields, dependent: :destroy, class_name: "#{name}CustomField"
after_save :save_custom_fields
CUSTOM_FIELDS_MAX_ITEMS = 100
CUSTOM_FIELDS_MAX_VALUE_LENGTH = 10_000_000
included do
attr_reader :preloaded_custom_fields
has_many :_custom_fields, dependent: :destroy, class_name: "#{name}CustomField"
validate :custom_fields_max_items, unless: :custom_fields_clean?
validate :custom_fields_value_length, unless: :custom_fields_clean?
after_save :save_custom_fields
def custom_fields_fk
@custom_fields_fk ||= "#{_custom_fields.reflect_on_all_associations(:belongs_to)[0].name}_id"
end
@ -133,6 +140,28 @@ module HasCustomFields
end
end
end
private
def custom_fields_max_items
if custom_fields.size > CUSTOM_FIELDS_MAX_ITEMS
errors.add(
:base,
I18n.t("custom_fields.validations.max_items", max_items_number: CUSTOM_FIELDS_MAX_ITEMS),
)
end
end
def custom_fields_value_length
return if custom_fields.values.all? { _1.to_s.size <= CUSTOM_FIELDS_MAX_VALUE_LENGTH }
errors.add(
:base,
I18n.t(
"custom_fields.validations.max_value_length",
max_value_length: CUSTOM_FIELDS_MAX_VALUE_LENGTH,
),
)
end
end
def reload(options = nil)

View File

@ -245,6 +245,10 @@ en:
errors:
<<: *errors
custom_fields:
validations:
max_items: "Maximum number of custom fields for this entity has been reached (%{max_items_number})"
max_value_length: "Maximum length for a custom field value has been reached (%{max_value_length})"
invite:
expired: "Your invite token has expired. Please <a href='%{base_url}/about'>contact staff</a>."
not_found: "Your invite token is invalid. Please <a href='%{base_url}/about'>contact staff</a>."

View File

@ -4,6 +4,8 @@
RSpec.describe Category do
fab!(:user) { Fabricate(:user) }
it_behaves_like "it has custom fields"
it { is_expected.to validate_presence_of :user_id }
it { is_expected.to validate_presence_of :name }

View File

@ -5,6 +5,8 @@ RSpec.describe Group do
let(:user) { Fabricate(:user) }
let(:group) { Fabricate(:group) }
it_behaves_like "it has custom fields"
describe "Validations" do
it { is_expected.to allow_value("#{"a" * 996}.com").for(:automatic_membership_email_domains) }
it do

View File

@ -7,6 +7,8 @@ RSpec.describe Post do
before { Oneboxer.stubs :onebox }
it_behaves_like "it has custom fields"
it { is_expected.to have_many(:reviewables).dependent(:destroy) }
describe "#hidden_reasons" do

View File

@ -16,6 +16,8 @@ RSpec.describe Topic do
Fabricate(:user, trust_level: SiteSetting.min_trust_level_to_allow_invite)
end
it_behaves_like "it has custom fields"
describe "Validations" do
let(:topic) { Fabricate.build(:topic) }

View File

@ -1,9 +1,11 @@
# frozen_string_literal: true
RSpec.describe User do
subject(:user) { Fabricate(:user, last_seen_at: 1.day.ago) }
fab!(:group) { Fabricate(:group) }
subject(:user) { Fabricate(:user, last_seen_at: 1.day.ago) }
it_behaves_like "it has custom fields"
def user_error_message(*keys)
I18n.t(:"activerecord.errors.models.user.attributes.#{keys.join(".")}")

View File

@ -0,0 +1,27 @@
# frozen_string_literal: true
RSpec.shared_examples_for "it has custom fields" do
let(:record) { described_class.new }
describe "Max number of custom fields" do
let(:custom_fields) { (1..101).to_a.product(["value"]).to_h }
before { record.custom_fields = custom_fields }
it "can't have more than 100 custom fields" do
expect(record).to be_invalid
expect(record.errors[:base]).to include(/Maximum number.*\(100\)/)
end
end
describe "Max length of a custom field" do
let(:bad_value) { "a" * 10_000_001 }
before { record.custom_fields[:my_custom_field] = bad_value }
it "can't have more than 10,000,000 characters" do
expect(record).to be_invalid
expect(record.errors[:base]).to include(/Maximum length.*\(10000000\)/)
end
end
end