Merge pull request #2770 from internetee/2750-admin-contact-cannot-be-a-minor-clean

2750 admin contact cannot be a minor clean
This commit is contained in:
Timo Võhmar 2025-03-24 14:13:26 +02:00 committed by GitHub
commit 5946afc729
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 189 additions and 52 deletions

View file

@ -1,4 +1,8 @@
class AdminDomainContact < DomainContact
include AgeValidation
validate :validate_contact_age
# rubocop:disable Metrics/AbcSize
# rubocop:disable Metrics/MethodLength
def self.replace(current_contact, new_contact)
@ -23,4 +27,14 @@ class AdminDomainContact < DomainContact
end
# rubocop:enable Metrics/AbcSize
# rubocop:enable Metrics/MethodLength
private
def validate_contact_age
return unless contact&.underage?
errors.add(:contact, I18n.t(
'activerecord.errors.models.admin_domain_contact.contact_too_young'
))
end
end

View file

@ -0,0 +1,53 @@
module AgeValidation
extend ActiveSupport::Concern
def underage?
case ident_type
when 'birthday'
underage_by_birthday?
when 'priv'
underage_by_estonian_id?
else
false
end
end
private
def underage_by_birthday?
birth_date = Date.parse(ident)
calculate_age(birth_date) < 18
end
def underage_by_estonian_id?
return false unless estonian_id?
birth_date = parse_estonian_id_birth_date(ident)
calculate_age(birth_date) < 18
end
def estonian_id?
ident_country_code == 'EE' && ident.match?(/^\d{11}$/)
end
def calculate_age(birth_date)
((Time.zone.now - birth_date.to_time) / 1.year.seconds).floor
end
def parse_estonian_id_birth_date(id_code)
century_number = id_code[0].to_i
year_digits = id_code[1..2]
month = id_code[3..4]
day = id_code[5..6]
birth_year = case century_number
when 1, 2 then "18#{year_digits}"
when 3, 4 then "19#{year_digits}"
when 5, 6 then "20#{year_digits}"
else
raise ArgumentError, "Invalid century number in Estonian ID"
end
Date.parse("#{birth_year}-#{month}-#{day}")
end
end

View file

@ -10,6 +10,7 @@ class Contact < ApplicationRecord
include Contact::Archivable
include Contact::CompanyRegister
include EmailVerifable
include AgeValidation
belongs_to :original, class_name: 'Contact'
belongs_to :registrar, required: true

View file

@ -12,6 +12,7 @@ class Domain < ApplicationRecord
include Domain::Releasable
include Domain::Disputable
include Domain::BulkUpdatable
include AgeValidation
PERIODS = [
['3 months', '3m'],
@ -866,7 +867,7 @@ class Domain < ApplicationRecord
return true if registrant.org? && Setting.admin_contacts_required_for_org
return false unless registrant.priv?
underage_registrant? && Setting.admin_contacts_required_for_minors
registrant.underage? && Setting.admin_contacts_required_for_minors
end
def require_tech_contacts?
@ -876,51 +877,7 @@ class Domain < ApplicationRecord
private
def underage_registrant?
case registrant.ident_type
when 'birthday'
underage_by_birthday?
when 'priv'
underage_by_estonian_id?
else
false
end
end
def underage_by_birthday?
birth_date = Date.parse(registrant.ident)
calculate_age(birth_date) < 18
end
def underage_by_estonian_id?
return false unless estonian_id?
birth_date = parse_estonian_id_birth_date(registrant.ident)
calculate_age(birth_date) < 18
end
def estonian_id?
registrant.ident_country_code == 'EE' && registrant.ident.match?(/^\d{11}$/)
end
def calculate_age(birth_date)
((Time.zone.now - birth_date.to_time) / 1.year.seconds).floor
end
def parse_estonian_id_birth_date(id_code)
century_number = id_code[0].to_i
year_digits = id_code[1..2]
month = id_code[3..4]
day = id_code[5..6]
birth_year = case century_number
when 1, 2 then "18#{year_digits}"
when 3, 4 then "19#{year_digits}"
when 5, 6 then "20#{year_digits}"
else
raise ArgumentError, "Invalid century number in Estonian ID"
end
Date.parse("#{birth_year}-#{month}-#{day}")
registrant.underage?
end
def validate_admin_contacts_ident_type

View file

@ -37,7 +37,12 @@ class Epp::Domain < Domain
# validate registrant here as well
([Contact.find(registrant.id)] + active_admins + active_techs).each do |x|
unless x.valid?
add_epp_error('2304', nil, nil, I18n.t(:contact_is_not_valid, value: x.try(:code)))
if x.class.name == 'AdminDomainContact' && x.contact.underage?
add_epp_error('2304', nil, nil, I18n.t('activerecord.errors.models.admin_domain_contact.contact_too_young'))
else
add_epp_error('2304', nil, nil, I18n.t(:contact_is_not_valid, value: x.try(:code)))
end
ok = false
end
end

View file

@ -160,6 +160,8 @@ en:
ipv6:
taken: 'has already been taken'
admin_domain_contact:
contact_too_young: "Administrative contact must be at least 18 years old"
attributes:
epp_domain: &epp_domain_attributes

View file

@ -23,11 +23,13 @@ class AdminAreaAdminUsersIntegrationTest < JavaScriptApplicationSystemTestCase
createNewAdminUser(true)
visit admin_admin_users_path
assert_selector 'a', text: 'test_user_name'
click_on 'test_user_name'
assert_text 'General'
click_on 'Edit'
assert_selector 'input[name*="password"]'
fill_in 'Password', with: 'test_password'
fill_in 'Password confirmation', with: 'test_password'

View file

@ -597,7 +597,7 @@ class EppDomainCreateBaseTest < EppTestCase
end
def test_registers_new_domain_with_required_attributes
Setting.admin_contacts_allowed_ident_type = { 'org' => true, 'priv' => true, 'birthday' => true }
Setting.admin_contacts_allowed_ident_type = { 'org' => true, 'priv' => true, 'birthday' => true }.to_json
now = Time.zone.parse('2010-07-05')
travel_to now
@ -647,7 +647,7 @@ class EppDomainCreateBaseTest < EppTestCase
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 }
Setting.admin_contacts_allowed_ident_type = { 'org' => false, 'priv' => true, 'birthday' => true }.to_json
end
def test_registers_domain_without_legaldoc_if_optout

View file

@ -1032,7 +1032,7 @@ class EppDomainUpdateBaseTest < EppTestCase
@domain.save!
# Change allowed types after domain is created
Setting.admin_contacts_allowed_ident_type = { 'birthday' => true, 'priv' => true, 'org' => false }
Setting.admin_contacts_allowed_ident_type = { 'birthday' => true, 'priv' => true, 'org' => false }.to_json
# Try to update domain with some other changes
request_xml = <<-XML
@ -1061,6 +1061,36 @@ class EppDomainUpdateBaseTest < EppTestCase
assert_epp_response :completed_successfully
end
def test_does_not_allow_underage_admin_contact
admin_contact = contacts(:william)
admin_contact.update!(
ident_type: 'priv',
ident: '61203150222',
ident_country_code: 'EE'
)
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>
<update>
<domain:update xmlns:domain="#{Xsd::Schema.filename(for_prefix: 'domain-ee', for_version: '1.2')}">
<domain:name>#{@domain.name}</domain:name>
<domain:add>
<domain:contact type="admin">#{admin_contact.code}</domain:contact>
</domain:add>
</domain:update>
</update>
</command>
</epp>
XML
post epp_update_path, params: { frame: request_xml },
headers: { 'HTTP_COOKIE' => 'session=api_bestnames' }
assert_epp_response :object_status_prohibits_operation
end
private
def assert_verification_and_notification_emails

View file

@ -14,7 +14,7 @@ class DomainVersionTest < ActiveSupport::TestCase
end
def test_assigns_creator_to_paper_trail_whodunnit
Setting.admin_contacts_allowed_ident_type = { 'org' => true, 'priv' => true, 'birthday' => true }
Setting.admin_contacts_allowed_ident_type = { 'org' => true, 'priv' => true, 'birthday' => true }.to_json
duplicate_domain = prepare_duplicate_domain
PaperTrail.request.whodunnit = @user.id_role_username

View file

@ -14,4 +14,70 @@ class DomainContactTest < ActiveSupport::TestCase
assert @domain_contact.value_typeahead, 'Jane'
end
def test_validates_admin_contact_age_with_birthday
admin_contact = contacts(:john)
admin_contact.update!(
ident_type: 'birthday',
ident: (Time.zone.now - 16.years).strftime('%Y-%m-%d')
)
domain_contact = AdminDomainContact.new(
domain: domains(:shop),
contact: admin_contact
)
assert_not domain_contact.valid?
assert_includes domain_contact.errors.full_messages,
'Contact Administrative contact must be at least 18 years old'
end
def test_validates_admin_contact_age_with_estonian_id
admin_contact = contacts(:john)
admin_contact.update!(
ident_type: 'priv',
ident: '61203150222',
ident_country_code: 'EE'
)
domain_contact = AdminDomainContact.new(
domain: domains(:shop),
contact: admin_contact
)
assert_not domain_contact.valid?
assert_includes domain_contact.errors.full_messages,
'Contact Administrative contact must be at least 18 years old'
end
def test_allows_adult_admin_contact_with_birthday
admin_contact = contacts(:john)
admin_contact.update!(
ident_type: 'birthday',
ident: (Time.zone.now - 20.years).strftime('%Y-%m-%d')
)
domain_contact = AdminDomainContact.new(
domain: domains(:shop),
contact: admin_contact
)
assert domain_contact.valid?
end
def test_allows_adult_admin_contact_with_estonian_id
admin_contact = contacts(:john)
admin_contact.update!(
ident_type: 'priv',
ident: '38903111310',
ident_country_code: 'EE'
)
domain_contact = AdminDomainContact.new(
domain: domains(:shop),
contact: admin_contact
)
assert domain_contact.valid?
end
end

View file

@ -587,7 +587,14 @@ class DomainTest < ActiveSupport::TestCase
assert domain.invalid?
assert_includes domain.errors.full_messages, 'Admin domain contacts Admin contacts count must be between 1-10'
domain.admin_domain_contacts.build(contact: contacts(:john))
admin_contact = contacts(:john)
admin_contact.update!(
ident_type: 'priv',
ident: '37810166020',
ident_country_code: 'EE'
)
domain.admin_domain_contacts.build(contact: admin_contact)
assert domain.valid?
end