From d695d95ad7ecc6b05ac892db46c0b733ffe91740 Mon Sep 17 00:00:00 2001 From: Artur Beljajev Date: Thu, 8 Nov 2018 17:55:58 +0200 Subject: [PATCH] Add contact disclosable attributes Closes #992 --- .../api/v1/registrant/contacts_controller.rb | 16 ++++++ app/models/concerns/contact/disclosable.rb | 26 ++++++++++ app/models/contact.rb | 1 + app/models/whois_record.rb | 29 ++++++----- .../admin/registrars/form/_billing.html.erb | 4 +- config/locales/admin/registrars.en.yml | 4 +- config/locales/contacts.en.yml | 2 + ...54921_add_contacts_disclosed_attributes.rb | 5 ++ db/structure.sql | 5 +- doc/registrant-api/v1/contact.md | 9 ++-- lib/serializers/registrant_api/contact.rb | 3 +- .../api/v1/registrant/contacts/update_test.rb | 49 ++++++++++++++++++- test/models/contact/disclosable_test.rb | 39 +++++++++++++++ test/models/contact_test.rb | 4 +- test/models/whois_record_test.rb | 18 ++++--- 15 files changed, 184 insertions(+), 30 deletions(-) create mode 100644 app/models/concerns/contact/disclosable.rb create mode 100644 db/migrate/20181108154921_add_contacts_disclosed_attributes.rb create mode 100644 test/models/contact/disclosable_test.rb diff --git a/app/controllers/api/v1/registrant/contacts_controller.rb b/app/controllers/api/v1/registrant/contacts_controller.rb index 2edbca0394..d262557296 100644 --- a/app/controllers/api/v1/registrant/contacts_controller.rb +++ b/app/controllers/api/v1/registrant/contacts_controller.rb @@ -45,6 +45,22 @@ def update contact.email = params[:email] if params[:email].present? contact.phone = params[:phone] if params[:phone].present? + # Needed to support passing empty array, which otherwise gets parsed to nil + # https://github.com/rails/rails/pull/13157 + reparsed_request_json = ActiveSupport::JSON.decode(request.body.string) + .with_indifferent_access + disclosed_attributes = reparsed_request_json[:disclosed_attributes] + + if disclosed_attributes + if contact.org? + error_msg = "Legal person's data cannot be concealed. Please remove this parameter." + render json: { errors: [{ disclosed_attributes: [error_msg] }] }, status: :bad_request + return + end + + contact.disclosed_attributes = disclosed_attributes + end + if Setting.address_processing && params[:address] address = Contact::Address.new(params[:address][:street], params[:address][:zip], diff --git a/app/models/concerns/contact/disclosable.rb b/app/models/concerns/contact/disclosable.rb new file mode 100644 index 0000000000..6c3b81849b --- /dev/null +++ b/app/models/concerns/contact/disclosable.rb @@ -0,0 +1,26 @@ +module Concerns + module Contact + module Disclosable + extend ActiveSupport::Concern + + class_methods do + attr_accessor :disclosable_attributes + end + + included do + self.disclosable_attributes = %w[name email] + validate :validate_disclosed_attributes + end + + private + + def validate_disclosed_attributes + return if disclosed_attributes.empty? + + has_undisclosable_attributes = (disclosed_attributes - self.class.disclosable_attributes) + .any? + errors.add(:disclosed_attributes, :invalid) if has_undisclosable_attributes + end + end + end +end diff --git a/app/models/contact.rb b/app/models/contact.rb index 9d166faee6..bb9ea67be2 100644 --- a/app/models/contact.rb +++ b/app/models/contact.rb @@ -4,6 +4,7 @@ class Contact < ActiveRecord::Base include UserEvents include Concerns::Contact::Transferable include Concerns::Contact::Identical + include Concerns::Contact::Disclosable belongs_to :original, class_name: self.name belongs_to :registrar, required: true diff --git a/app/models/whois_record.rb b/app/models/whois_record.rb index 2c6f79fcd3..b2a0349aa5 100644 --- a/app/models/whois_record.rb +++ b/app/models/whois_record.rb @@ -52,22 +52,18 @@ def generate_json h[:email] = registrant.email h[:registrant_changed] = registrant.updated_at.try(:to_s, :iso8601) + h[:registrant_disclosed_attributes] = registrant.disclosed_attributes h[:admin_contacts] = [] - domain.admin_contacts.each do |ac| - h[:admin_contacts] << { - name: ac.name, - email: ac.email, - changed: ac.updated_at.try(:to_s, :iso8601) - } + + domain.admin_contacts.each do |contact| + h[:admin_contacts] << contact_json_hash(contact) end + h[:tech_contacts] = [] - domain.tech_contacts.each do |tc| - h[:tech_contacts] << { - name: tc.name, - email: tc.email, - changed: tc.updated_at.try(:to_s, :iso8601) - } + + domain.tech_contacts.each do |contact| + h[:tech_contacts] << contact_json_hash(contact) end # update registar triggers when adding new attributes @@ -109,4 +105,13 @@ def destroy_whois_record def disclaimer_text Setting.registry_whois_disclaimer end + + def contact_json_hash(contact) + { + name: contact.name, + email: contact.email, + changed: contact.updated_at.try(:to_s, :iso8601), + disclosed_attributes: contact.disclosed_attributes, + } + end end diff --git a/app/views/admin/registrars/form/_billing.html.erb b/app/views/admin/registrars/form/_billing.html.erb index 03dfc22c6c..b240094400 100644 --- a/app/views/admin/registrars/form/_billing.html.erb +++ b/app/views/admin/registrars/form/_billing.html.erb @@ -51,7 +51,7 @@
<% if f.object.new_record? %>
- <%= t '.no_reference_no_hint' %> + <%= t '.no_reference_number_hint' %>
<% else %>
@@ -60,7 +60,7 @@
<%= f.text_field :reference_no, disabled: true, - title: t('.disabled_reference_no_hint'), + title: t('.disabled_reference_number_hint'), class: 'form-control' %>
<% end %> diff --git a/config/locales/admin/registrars.en.yml b/config/locales/admin/registrars.en.yml index 47a443e060..43f607911f 100644 --- a/config/locales/admin/registrars.en.yml +++ b/config/locales/admin/registrars.en.yml @@ -35,8 +35,8 @@ en: billing: header: Billing - no_reference_no_hint: Reference number will be generated automatically - disabled_reference_no_hint: Reference number cannot be changed + no_reference_number_hint: Reference number will be generated automatically + disabled_reference_number_hint: Reference number cannot be changed preferences: header: Preferences diff --git a/config/locales/contacts.en.yml b/config/locales/contacts.en.yml index 47589039bc..cdfe2277d4 100644 --- a/config/locales/contacts.en.yml +++ b/config/locales/contacts.en.yml @@ -26,3 +26,5 @@ en: not_uniq: 'not uniq' country_code: invalid: Country code is not valid, should be in ISO_3166-1 alpha 2 format (%{value}) + disclosed_attributes: + invalid: contain unsupported attribute(s) diff --git a/db/migrate/20181108154921_add_contacts_disclosed_attributes.rb b/db/migrate/20181108154921_add_contacts_disclosed_attributes.rb new file mode 100644 index 0000000000..f819fd2e2f --- /dev/null +++ b/db/migrate/20181108154921_add_contacts_disclosed_attributes.rb @@ -0,0 +1,5 @@ +class AddContactsDisclosedAttributes < ActiveRecord::Migration + def change + add_column :contacts, :disclosed_attributes, :string, array: true, default: [], null: false + end +end diff --git a/db/structure.sql b/db/structure.sql index a04a0d462f..98d1fbd26f 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -671,7 +671,8 @@ CREATE TABLE public.contacts ( ident_updated_at timestamp without time zone, upid integer, up_date timestamp without time zone, - uuid uuid DEFAULT public.gen_random_uuid() NOT NULL + uuid uuid DEFAULT public.gen_random_uuid() NOT NULL, + disclosed_attributes character varying[] DEFAULT '{}'::character varying[] NOT NULL ); @@ -4861,3 +4862,5 @@ INSERT INTO schema_migrations (version) VALUES ('20181001090536'); INSERT INTO schema_migrations (version) VALUES ('20181002090319'); +INSERT INTO schema_migrations (version) VALUES ('20181108154921'); + diff --git a/doc/registrant-api/v1/contact.md b/doc/registrant-api/v1/contact.md index 6d7f04cbd8..87519c0aac 100644 --- a/doc/registrant-api/v1/contact.md +++ b/doc/registrant-api/v1/contact.md @@ -91,13 +91,14 @@ Content-Type: application/json "auth_info": "password", "statuses": [ "ok" - ] + ], + "disclosed_attributes": ["name"] } ``` ## PATCH /api/v1/registrant/contacts/$UUID -Update contact details for a contact. +Update contact. #### Parameters @@ -112,6 +113,7 @@ Update contact details for a contact. | address[city] | false | String | | New city name | | address[state] | false | String | | New state name | | address[country_code] | false | String | | New country code in 2 letter format (ISO 3166-1 alpha-2) | +| disclosed_attributes | false | Array | | Possible values: "name", "email" #### Request @@ -132,7 +134,8 @@ Content-type: application/json "city":"New City", "state":"New state", "country_code":"LV" - } + }, + "disclosed_attributes": ["name"] } ``` diff --git a/lib/serializers/registrant_api/contact.rb b/lib/serializers/registrant_api/contact.rb index 6c02909c37..dd36b44000 100644 --- a/lib/serializers/registrant_api/contact.rb +++ b/lib/serializers/registrant_api/contact.rb @@ -28,7 +28,8 @@ def to_json country_code: contact.country_code, }, auth_info: contact.auth_info, - statuses: contact.statuses + statuses: contact.statuses, + disclosed_attributes: contact.disclosed_attributes, } end end diff --git a/test/integration/api/v1/registrant/contacts/update_test.rb b/test/integration/api/v1/registrant/contacts/update_test.rb index c828081091..abf483d9c7 100644 --- a/test/integration/api/v1/registrant/contacts/update_test.rb +++ b/test/integration/api/v1/registrant/contacts/update_test.rb @@ -129,6 +129,51 @@ def test_address_cannot_be_updated_when_disabled symbolize_names: true) end + def test_disclose_private_persons_data + @contact.update!(ident_type: Contact::PRIV, + disclosed_attributes: %w[]) + + patch api_v1_registrant_contact_path(@contact.uuid), { disclosed_attributes: %w[name] }.to_json, + 'HTTP_AUTHORIZATION' => auth_token, + 'Accept' => Mime::JSON, + 'Content-Type' => Mime::JSON.to_s + @contact.reload + + assert_response :ok + assert_equal %w[name], @contact.disclosed_attributes + end + + def test_conceal_private_persons_data + @contact.update!(ident_type: Contact::PRIV, disclosed_attributes: %w[name]) + + patch api_v1_registrant_contact_path(@contact.uuid), { disclosed_attributes: [] }.to_json, + { 'HTTP_AUTHORIZATION' => auth_token, + 'Accept' => Mime::JSON, + 'Content-Type' => Mime::JSON.to_s } + + @contact.reload + + assert_response :ok + assert_empty @contact.disclosed_attributes + end + + def test_legal_persons_data_cannot_be_concealed + @contact.update!(ident_type: Contact::ORG, + disclosed_attributes: %w[]) + + assert_no_changes -> { @contact.disclosed_attributes } do + patch api_v1_registrant_contact_path(@contact.uuid), { disclosed_attributes: %w[name] }.to_json, + 'HTTP_AUTHORIZATION' => auth_token, + 'Accept' => Mime::JSON, + 'Content-Type' => Mime::JSON.to_s + @contact.reload + end + assert_response :bad_request + error_msg = "Legal person's data cannot be concealed. Please remove this parameter." + assert_equal ({ errors: [{ disclosed_attributes: [error_msg] }] }), + JSON.parse(response.body, symbolize_names: true) + end + def test_return_contact_details patch api_v1_registrant_contact_path(@contact.uuid), { name: 'new name' }.to_json, 'HTTP_AUTHORIZATION' => auth_token, @@ -153,7 +198,9 @@ def test_return_contact_details country_code: @contact.country_code, }, auth_info: @contact.auth_info, - statuses: @contact.statuses }), JSON.parse(response.body, symbolize_names: true) + statuses: @contact.statuses, + disclosed_attributes: @contact.disclosed_attributes }), + JSON.parse(response.body, symbolize_names: true) end def test_errors diff --git a/test/models/contact/disclosable_test.rb b/test/models/contact/disclosable_test.rb new file mode 100644 index 0000000000..4b944513d4 --- /dev/null +++ b/test/models/contact/disclosable_test.rb @@ -0,0 +1,39 @@ +require 'test_helper' + +class ContactDisclosableTest < ActiveSupport::TestCase + setup do + @contact = contacts(:john) + @original_disclosable_attributes = Contact.disclosable_attributes + end + + teardown do + Contact.disclosable_attributes = @original_disclosable_attributes + end + + def test_no_disclosed_attributes_by_default + assert_empty Contact.new.disclosed_attributes + end + + def test_disclosable_attributes + assert_equal %w[name email], Contact.disclosable_attributes + end + + def test_valid_without_disclosed_attributes + @contact.disclosed_attributes = [] + assert @contact.valid? + end + + def test_invalid_when_attribute_is_not_disclosable + Contact.disclosable_attributes = %w[some disclosable] + @contact.disclosed_attributes = %w[some undisclosable] + + assert @contact.invalid? + assert_includes @contact.errors.get(:disclosed_attributes), 'contain unsupported attribute(s)' + end + + def test_valid_when_attribute_is_disclosable + Contact.disclosable_attributes = %w[some disclosable] + @contact.disclosed_attributes = %w[disclosable] + assert @contact.valid? + end +end diff --git a/test/models/contact_test.rb b/test/models/contact_test.rb index 7a383bbae4..3657dcc594 100644 --- a/test/models/contact_test.rb +++ b/test/models/contact_test.rb @@ -6,7 +6,7 @@ class ContactTest < ActiveSupport::TestCase end def test_valid_fixture - assert @contact.valid? + assert @contact.valid?, proc { @contact.errors.full_messages } end def test_invalid_without_email @@ -48,4 +48,4 @@ def test_address assert_equal 'EE', @contact.country_code assert_equal address, @contact.address end -end \ No newline at end of file +end diff --git a/test/models/whois_record_test.rb b/test/models/whois_record_test.rb index a806a26d5a..fcd6cfa5c8 100644 --- a/test/models/whois_record_test.rb +++ b/test/models/whois_record_test.rb @@ -39,12 +39,13 @@ def test_whois_record_has_no_disclaimer_if_Setting_is_blank end def test_generates_json_with_registrant - registrant = contacts(:john).becomes(Registrant) - registrant.update!(name: 'John', kind: 'priv', email: 'john@shop.test', - updated_at: Time.zone.parse('2010-07-05')) + contact = contacts(:john) + contact.update!(name: 'John', kind: 'priv', email: 'john@shop.test', + updated_at: Time.zone.parse('2010-07-05'), + disclosed_attributes: %w[name]) domain = domains(:shop) - domain.update!(registrant: registrant) + domain.update!(registrant: contact.becomes(Registrant)) whois_record = whois_records(:shop) whois_record.update!(json: {}) @@ -54,12 +55,14 @@ def test_generates_json_with_registrant assert_equal 'priv', generated_json[:registrant_kind] assert_equal 'john@shop.test', generated_json[:email] assert_equal '2010-07-05T00:00:00+03:00', generated_json[:registrant_changed] + assert_equal %w[name], generated_json[:registrant_disclosed_attributes] end def test_generates_json_with_admin_contacts contact = contacts(:john) contact.update!(name: 'John', email: 'john@shop.test', - updated_at: Time.zone.parse('2010-07-05')) + updated_at: Time.zone.parse('2010-07-05'), + disclosed_attributes: %w[name]) domain = domains(:shop) domain.admin_contacts = [contact] @@ -71,12 +74,14 @@ def test_generates_json_with_admin_contacts assert_equal 'John', admin_contact_json[:name] assert_equal 'john@shop.test', admin_contact_json[:email] assert_equal '2010-07-05T00:00:00+03:00', admin_contact_json[:changed] + assert_equal %w[name], admin_contact_json[:disclosed_attributes] end def test_generates_json_with_tech_contacts contact = contacts(:john) contact.update!(name: 'John', email: 'john@shop.test', - updated_at: Time.zone.parse('2010-07-05')) + updated_at: Time.zone.parse('2010-07-05'), + disclosed_attributes: %w[name]) domain = domains(:shop) domain.tech_contacts = [contact] @@ -88,5 +93,6 @@ def test_generates_json_with_tech_contacts assert_equal 'John', tech_contact_json[:name] assert_equal 'john@shop.test', tech_contact_json[:email] assert_equal '2010-07-05T00:00:00+03:00', tech_contact_json[:changed] + assert_equal %w[name], tech_contact_json[:disclosed_attributes] end end