Skip to content

Commit

Permalink
feat: add admin contact ident type validation
Browse files Browse the repository at this point in the history
- Add new setting for allowed admin contact ident types
- Add validation for admin contact ident types on domain create/update
- Add UI controls for managing allowed ident types
- Add tests for new validation rules
- Update domain model to respect new settings

The changes allow configuring which identification types (private person,
organization, birthday) are allowed for administrative contacts. This is
enforced when creating new domains or adding new admin contacts.
  • Loading branch information
OlegPhenomenon committed Feb 3, 2025
1 parent b641235 commit f297859
Show file tree
Hide file tree
Showing 12 changed files with 346 additions and 11 deletions.
19 changes: 16 additions & 3 deletions app/controllers/admin/settings_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,25 @@ def create

def casted_settings
settings = {}

params[:settings].each do |k, v|
settings[k] = { value: v }
setting = SettingEntry.find(k)
value = if setting.format == 'array'
processed_hash = available_options.each_with_object({}) do |option, hash|
hash[option] = (v[option] == "true")
end
processed_hash.to_json
else
v
end
settings[k] = { value: value }
end

settings
end

def available_options
%w[birthday priv org]
end
end
end
20 changes: 18 additions & 2 deletions app/models/domain.rb
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,8 @@ def self.tech_contacts_validation_rules(for_org:)
object_count: admin_contacts_validation_rules(for_org: false),
unless: :require_admin_contacts?

validate :validate_admin_contacts_ident_type, on: :create

validates :tech_domain_contacts,
object_count: tech_contacts_validation_rules(for_org: true),
if: :require_tech_contacts?
Expand Down Expand Up @@ -857,10 +859,10 @@ def self.swap_elements(array, indexes)
end

def require_admin_contacts?
return true if registrant.org?
return true if registrant.org? && Setting.admin_contacts_required_for_org
return false unless registrant.priv?

underage_registrant?
underage_registrant? && Setting.admin_contacts_required_for_minors
end

def require_tech_contacts?
Expand Down Expand Up @@ -916,4 +918,18 @@ def parse_estonian_id_birth_date(id_code)

Date.parse("#{birth_year}-#{month}-#{day}")
end

def validate_admin_contacts_ident_type
allowed_types = Setting.admin_contacts_allowed_ident_type
return if allowed_types.blank?

admin_contacts.each do |contact|
next if allowed_types[contact.ident_type] == true

errors.add(:admin_contacts, I18n.t(
'activerecord.errors.models.domain.admin_contact_invalid_ident_type',
ident_type: contact.ident_type
))
end
end
end
26 changes: 22 additions & 4 deletions app/models/epp/domain.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,23 @@ def validate_contacts
ok = false
end
end

# Validate admin contacts ident type only for new domains or new admin contacts
allowed_types = Setting.admin_contacts_allowed_ident_type
if allowed_types.present?
active_admins.each do |admin_contact|
next if !new_record? && admin_contact.persisted? && !admin_contact.changed?

contact = admin_contact.contact
unless allowed_types[contact.ident_type] == true
add_epp_error('2306', 'contact', contact.code,
I18n.t('activerecord.errors.models.domain.admin_contact_invalid_ident_type',
ident_type: contact.ident_type))
ok = false
end
end
end

ok
end

Expand Down Expand Up @@ -95,7 +112,8 @@ def epp_code_map
[:base, :key_data_not_allowed],
[:period, :not_a_number],
[:period, :not_an_integer],
[:registrant, :cannot_be_missing]
[:registrant, :cannot_be_missing],
[:admin_contacts, :invalid_ident_type]
],
'2308' => [
[:base, :domain_name_blocked, { value: { obj: 'name', val: name_dirty } }],
Expand Down Expand Up @@ -414,15 +432,15 @@ def admin_contacts_validation_rules(for_org:)
end

def require_admin_contacts?
return true if registrant.org?
return true if registrant.org? && Setting.admin_contacts_required_for_org
return false unless registrant.priv?

underage_registrant?
underage_registrant? && Setting.admin_contacts_required_for_minors
end

def tech_contacts_validation_rules(for_org:)
{
min: 0, # Технический контакт опционален для всех
min: 0,
max: -> { Setting.tech_contacts_max_count }
}
end
Expand Down
13 changes: 12 additions & 1 deletion app/models/setting_entry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,17 @@ def hash_format
end

def array_format
JSON.parse(value).to_a
begin
if value.is_a?(String)
JSON.parse(value)
elsif value.is_a?(Hash)
value
else
{ 'birthday' => true, 'priv' => true, 'org' => true }
end
rescue JSON::ParserError => e
puts "JSON Parse error: #{e.message}"
{ 'birthday' => true, 'priv' => true, 'org' => true }
end
end
end
28 changes: 27 additions & 1 deletion app/views/admin/settings/_setting_row.haml
Original file line number Diff line number Diff line change
@@ -1,6 +1,32 @@
%tr{class: (@errors && @errors.has_key?(setting.code) && "danger")}
%td.col-md-6= setting.code.humanize
- if [TrueClass, FalseClass].include?(setting.retrieve.class)
- if setting.format == 'array'
%td.col-md-6
- available_options = ['birthday', 'priv', 'org']
- begin
- raw_value = setting.retrieve
- current_values = if raw_value.is_a?(Hash)
- raw_value
- elsif raw_value.is_a?(Array) && raw_value.first.is_a?(Array)
- Hash[raw_value]
- elsif raw_value.is_a?(Array)
- available_options.each_with_object({}) { |opt, hash| hash[opt] = raw_value.include?(opt) }
- else
- begin
- parsed = JSON.parse(raw_value.to_s)
- parsed.is_a?(Hash) ? parsed : available_options.each_with_object({}) { |opt, hash| hash[opt] = true }
- rescue => e
- available_options.each_with_object({}) { |opt, hash| hash[opt] = true }
.row
- available_options.each do |option|
.col-md-4
.checkbox
%label
= check_box_tag "settings[#{setting.id}][#{option}]", "true", current_values[option],
id: "setting_#{setting.id}_#{option}",
data: { value: current_values[option] }
= option.humanize
- elsif [TrueClass, FalseClass].include?(setting.retrieve.class)
%td.col-md-6
= hidden_field_tag("[settings][#{setting.id}]", '', id: nil)
= check_box_tag("[settings][#{setting.id}]", true, setting.retrieve)
Expand Down
1 change: 1 addition & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ en:

domain:
<<: *epp_domain_ar_attributes
admin_contact_invalid_ident_type: "Administrative contact with identification type '%{ident_type}' is not allowed"

nameserver:
attributes:
Expand Down
13 changes: 13 additions & 0 deletions db/seeds.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@
SettingEntry.create(code: 'directo_sales_agent', value: 'HELEN', format: 'string', group: 'billing')
SettingEntry.create(code: 'admin_contacts_min_count', value: '1', format: 'integer', group: 'domain_validation')
SettingEntry.create(code: 'admin_contacts_max_count', value: '10', format: 'integer', group: 'domain_validation')
SettingEntry.create(code: 'admin_contacts_required_for_org', value: 'true', format: 'boolean', group: 'domain_validation')
SettingEntry.create(code: 'admin_contacts_required_for_minors', value: 'true', format: 'boolean', group: 'domain_validation')
SettingEntry.create(
code: 'admin_contacts_allowed_ident_type',
value: {
'birthday' => true,
'priv' => true,
'org' => true
}.to_json,
format: 'array',
group: 'domain_validation'
)

SettingEntry.create(code: 'tech_contacts_min_count', value: '1', format: 'integer', group: 'domain_validation')
SettingEntry.create(code: 'tech_contacts_max_count', value: '10', format: 'integer', group: 'domain_validation')
SettingEntry.create(code: 'orphans_contacts_in_months', value: '6', format: 'integer', group: 'domain_validation')
Expand Down
24 changes: 24 additions & 0 deletions test/fixtures/setting_entries.yml
Original file line number Diff line number Diff line change
Expand Up @@ -469,3 +469,27 @@ ip_whitelist_max_count:
format: integer
created_at: <%= Time.zone.parse('2010-07-05') %>
updated_at: <%= Time.zone.parse('2010-07-05') %>

admin_contacts_required_for_org:
code: admin_contacts_required_for_org
value: 'true'
group: domain_validation
format: boolean
created_at: <%= Time.zone.parse('2010-07-05') %>
updated_at: <%= Time.zone.parse('2010-07-05') %>

admin_contacts_required_for_minors:
code: admin_contacts_required_for_minors
value: 'true'
group: domain_validation
format: boolean
created_at: <%= Time.zone.parse('2010-07-05') %>
updated_at: <%= Time.zone.parse('2010-07-05') %>

admin_contacts_allowed_ident_type:
code: admin_contacts_allowed_ident_type
value: '{"birthday":true,"priv":true,"org":false}'
group: domain_validation
format: array
created_at: <%= Time.zone.parse('2010-07-05') %>
updated_at: <%= Time.zone.parse('2010-07-05') %>
78 changes: 78 additions & 0 deletions test/integration/epp/domain/create/base_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,8 @@ def test_registers_domain_for_adult_estonian_id_without_admin_contact
end

def test_registers_new_domain_with_required_attributes
Setting.admin_contacts_allowed_ident_type = { 'org' => true, 'priv' => true, 'birthday' => true }

now = Time.zone.parse('2010-07-05')
travel_to now
name = "new.#{dns_zones(:one).origin}"
Expand Down Expand Up @@ -644,6 +646,8 @@ def test_registers_new_domain_with_required_attributes

default_registration_period = 1.year + 1.day
assert_equal now + default_registration_period, domain.expire_time

Setting.admin_contacts_allowed_ident_type = { 'org' => false, 'priv' => true, 'birthday' => true }
end

def test_registers_domain_without_legaldoc_if_optout
Expand Down Expand Up @@ -1108,4 +1112,78 @@ def test_returns_error_response_if_throttled
ENV["shunter_default_threshold"] = '10000'
ENV["shunter_enabled"] = 'false'
end

def test_does_not_register_domain_with_invalid_admin_contact_ident_type
name = "new.#{dns_zones(:one).origin}"
contact = contacts(:john)
registrant = contact.becomes(Registrant)
admin_contact = contacts(:william)
admin_contact.update!(ident_type: 'org')

request_xml = <<-XML
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<epp xmlns="#{Xsd::Schema.filename(for_prefix: 'epp-ee', for_version: '1.0')}">
<command>
<create>
<domain:create xmlns:domain="#{Xsd::Schema.filename(for_prefix: 'domain-ee', for_version: '1.2')}">
<domain:name>#{name}</domain:name>
<domain:registrant>#{registrant.code}</domain:registrant>
<domain:contact type="admin">#{admin_contact.code}</domain:contact>
</domain:create>
</create>
<extension>
<eis:extdata xmlns:eis="#{Xsd::Schema.filename(for_prefix: 'eis', for_version: '1.0')}">
<eis:legalDocument type="pdf">#{'test' * 2000}</eis:legalDocument>
</eis:extdata>
</extension>
</command>
</epp>
XML

assert_no_difference 'Domain.count' do
post epp_create_path, params: { frame: request_xml },
headers: { 'HTTP_COOKIE' => 'session=api_bestnames' }
end

response_xml = Nokogiri::XML(response.body)
assert_correct_against_schema response_xml
assert_epp_response :parameter_value_policy_error
end

def test_registers_domain_with_valid_admin_contact_ident_type
name = "new.#{dns_zones(:one).origin}"
contact = contacts(:john)
registrant = contact.becomes(Registrant)
admin_contact = contacts(:william)
admin_contact.update!(ident_type: 'priv')

request_xml = <<-XML
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<epp xmlns="#{Xsd::Schema.filename(for_prefix: 'epp-ee', for_version: '1.0')}">
<command>
<create>
<domain:create xmlns:domain="#{Xsd::Schema.filename(for_prefix: 'domain-ee', for_version: '1.2')}">
<domain:name>#{name}</domain:name>
<domain:registrant>#{registrant.code}</domain:registrant>
<domain:contact type="admin">#{admin_contact.code}</domain:contact>
</domain:create>
</create>
<extension>
<eis:extdata xmlns:eis="#{Xsd::Schema.filename(for_prefix: 'eis', for_version: '1.0')}">
<eis:legalDocument type="pdf">#{'test' * 2000}</eis:legalDocument>
</eis:extdata>
</extension>
</command>
</epp>
XML

assert_difference 'Domain.count' do
post epp_create_path, params: { frame: request_xml },
headers: { 'HTTP_COOKIE' => 'session=api_bestnames' }
end

response_xml = Nokogiri::XML(response.body)
assert_correct_against_schema response_xml
assert_epp_response :completed_successfully
end
end
Loading

0 comments on commit f297859

Please sign in to comment.