From e50cf7d2001b0d96a3a01af894b1601e20220fe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl=20Erik=20=C3=95unapuu?= Date: Wed, 12 Feb 2020 15:33:22 +0200 Subject: [PATCH 01/20] Registrar: Allow to view other contacts of domain --- app/controllers/registrant/contacts_controller.rb | 3 ++- app/models/contact.rb | 11 ++++++++--- app/views/registrant/contacts/show/_domains.html.erb | 2 +- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/app/controllers/registrant/contacts_controller.rb b/app/controllers/registrant/contacts_controller.rb index af7136ce9..e690b50f4 100644 --- a/app/controllers/registrant/contacts_controller.rb +++ b/app/controllers/registrant/contacts_controller.rb @@ -5,7 +5,8 @@ class Registrant::ContactsController < RegistrantController skip_authorization_check only: %i[edit update] def show - @contact = current_user_contacts.find(params[:id]) + @contact = domain.contacts.find(params[:id]) + @requester_contact = domain.contacts.find_by(ident: current_registrant_user.ident).id authorize! :read, @contact end diff --git a/app/models/contact.rb b/app/models/contact.rb index 558292dbd..ddf39c9bb 100644 --- a/app/models/contact.rb +++ b/app/models/contact.rb @@ -415,7 +415,7 @@ class Contact < ApplicationRecord # if total is smaller than needed, the load more # we also need to sort by valid_to # todo: extract to drapper. Then we can remove Domain#roles - def all_domains(page: nil, per: nil, params:) + def all_domains(page: nil, per: nil, params:, requester:) # compose filter sql filter_sql = case params[:domain_filter] when "Registrant".freeze @@ -431,9 +431,14 @@ class Contact < ApplicationRecord sort = Domain.column_names.include?(sorts.first) ? sorts.first : "valid_to" order = {"asc"=>"desc", "desc"=>"asc"}[sorts.second] || "desc" - # fetch domains - domains = Domain.where("domains.id IN (#{filter_sql})") + if requester + requester_domains = Contact.find(requester).domains + domains = requester_domains.where("domains.id IN (#{filter_sql})") + else + domains = Domain.where("domains.id IN (#{filter_sql})") + end + domains = domains.includes(:registrar).page(page).per(per) if sorts.first == "registrar_name".freeze diff --git a/app/views/registrant/contacts/show/_domains.html.erb b/app/views/registrant/contacts/show/_domains.html.erb index 167ab1240..d783b55b2 100644 --- a/app/views/registrant/contacts/show/_domains.html.erb +++ b/app/views/registrant/contacts/show/_domains.html.erb @@ -1,5 +1,5 @@ <% domains = contact.all_domains(page: params[:domain_page], per: 20, - params: domain_filter_params.to_h) %> + params: domain_filter_params.to_h, requester: @requester_contact) %>
From 53c466e6e5dd95d1d29503d2b88b4b41c0917cfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl=20Erik=20=C3=95unapuu?= Date: Mon, 4 May 2020 14:53:50 +0300 Subject: [PATCH 02/20] Default fixture for business addr --- app/models/contact.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/contact.rb b/app/models/contact.rb index ac6facbe8..aa2f32140 100644 --- a/app/models/contact.rb +++ b/app/models/contact.rb @@ -260,8 +260,8 @@ class Contact < ApplicationRecord private def registrant_user_indirect_contacts(registrant_user) - ident = registrant_user.companies.collect(&:registration_number) - + # ident = registrant_user.companies.collect(&:registration_number) + ident = [1234] where(ident_type: ORG, ident: ident, ident_country_code: registrant_user.country.alpha2) From 7930c4d8b69392578dfd95ccc0f01f4bb216773d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl=20Erik=20=C3=95unapuu?= Date: Mon, 4 May 2020 15:38:48 +0300 Subject: [PATCH 03/20] Show only domains for contact that requester contact has access to --- app/models/contact.rb | 76 +++++++++++++++++++++++++------------------ 1 file changed, 45 insertions(+), 31 deletions(-) diff --git a/app/models/contact.rb b/app/models/contact.rb index aa2f32140..488e72335 100644 --- a/app/models/contact.rb +++ b/app/models/contact.rb @@ -260,8 +260,8 @@ class Contact < ApplicationRecord private def registrant_user_indirect_contacts(registrant_user) - # ident = registrant_user.companies.collect(&:registration_number) - ident = [1234] + ident = registrant_user.companies.collect(&:registration_number) + where(ident_type: ORG, ident: ident, ident_country_code: registrant_user.country.alpha2) @@ -416,49 +416,63 @@ class Contact < ApplicationRecord # we also need to sort by valid_to # todo: extract to drapper. Then we can remove Domain#roles def all_domains(page: nil, per: nil, params:, requester:) - # compose filter sql - filter_sql = case params[:domain_filter] - when "Registrant".freeze - %Q{select id from domains where registrant_id=#{id}} - when AdminDomainContact.to_s, TechDomainContact.to_s - %Q{select domain_id from domain_contacts where contact_id=#{id} AND type='#{params[:domain_filter]}'} - else - %Q{select domain_id from domain_contacts where contact_id=#{id} UNION select id from domains where registrant_id=#{id}} - end + filter_sql = qualified_domain_ids(params[:domain_filter]) # get sorting rules sorts = params.fetch(:sort, {}).first || [] - sort = Domain.column_names.include?(sorts.first) ? sorts.first : "valid_to" - order = {"asc"=>"desc", "desc"=>"asc"}[sorts.second] || "desc" + sort = %w[name registrar_name valid_to].include?(sorts.first) ? sorts.first : 'valid_to' + order = %w[asc desc].include?(sorts.second) ? sorts.second : 'desc' # fetch domains - if requester - requester_domains = Contact.find(requester).domains - domains = requester_domains.where("domains.id IN (#{filter_sql})") - else - domains = Domain.where("domains.id IN (#{filter_sql})") - end - + domains = qualified_domain_name_list(requester, filter_sql) domains = domains.includes(:registrar).page(page).per(per) - if sorts.first == "registrar_name".freeze - # using small rails hack to generate outer join - domains = domains.includes(:registrar).where.not(registrars: {id: nil}).order("registrars.name #{order} NULLS LAST") - else - domains = domains.order("#{sort} #{order} NULLS LAST") - end - - + # using small rails hack to generate outer join + domains = if sorts.first == 'registrar_name'.freeze + domains.includes(:registrar).where.not(registrars: { id: nil }) + .order("registrars.name #{order} NULLS LAST") + else + domains.order("#{sort} #{order} NULLS LAST") + end # adding roles. Need here to make faster sqls domain_c = Hash.new([]) - registrant_domains.where(id: domains.map(&:id)).each{|d| domain_c[d.id] |= ["Registrant".freeze] } - DomainContact.where(contact_id: id, domain_id: domains.map(&:id)).each{|d| domain_c[d.domain_id] |= [d.type] } - domains.each{|d| d.roles = domain_c[d.id].uniq} + registrant_domains.where(id: domains.map(&:id)).each do |d| + domain_c[d.id] |= ['Registrant'.freeze] + end + + DomainContact.where(contact_id: id, domain_id: domains.map(&:id)).each do |d| + domain_c[d.domain_id] |= [d.type] + end + + domains.each { |d| d.roles = domain_c[d.id].uniq } domains end + def qualified_domain_name_list(requester, filter_sql) + if requester + requester_domains = Contact.find(requester).domains + domains = requester_domains.where('domains.id IN (?)', filter_sql) + else + domains = Domain.where('domains.id IN (?)', filter_sql) + end + + domains + end + + def qualified_domain_ids(domain_filter) + registrant_ids = Domain.select('id').where(registrant: id).pluck(:id) + return registrant_ids if domain_filter == 'Registrant' + + if %w[AdminDomainContact TechDomainContact].include? domain_filter + DomainContact.select('domain_id').where(contact_id: id, type: domain_filter) + else + (DomainContact.select('domain_id').where(contact_id: id).pluck(:id) + + registrant_ids).uniq + end + end + def update_prohibited? (statuses & [ CLIENT_UPDATE_PROHIBITED, From d0053def99d84429a46fc9e682b7cc547c2ac1c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl=20Erik=20=C3=95unapuu?= Date: Tue, 5 May 2020 12:35:27 +0300 Subject: [PATCH 04/20] Find requester contact id solely by ident --- app/controllers/registrant/contacts_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/registrant/contacts_controller.rb b/app/controllers/registrant/contacts_controller.rb index e690b50f4..1ad403248 100644 --- a/app/controllers/registrant/contacts_controller.rb +++ b/app/controllers/registrant/contacts_controller.rb @@ -6,7 +6,7 @@ class Registrant::ContactsController < RegistrantController def show @contact = domain.contacts.find(params[:id]) - @requester_contact = domain.contacts.find_by(ident: current_registrant_user.ident).id + @requester_contact = Contact.find_by(ident: current_registrant_user.ident).id authorize! :read, @contact end From bd7b6ddb73d372841b0f7dae02708f3be034ceaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl=20Erik=20=C3=95unapuu?= Date: Tue, 5 May 2020 12:36:04 +0300 Subject: [PATCH 05/20] Create test for viewing other domain contacts --- .../registrant_area/contacts_test.rb | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 test/integration/registrant_area/contacts_test.rb diff --git a/test/integration/registrant_area/contacts_test.rb b/test/integration/registrant_area/contacts_test.rb new file mode 100644 index 000000000..c906cd026 --- /dev/null +++ b/test/integration/registrant_area/contacts_test.rb @@ -0,0 +1,19 @@ +require 'test_helper' + +class RegistrantAreaContactsIntegrationTest < ApplicationIntegrationTest + setup do + @domain = domains(:shop) + @registrant = users(:registrant) + sign_in @registrant + end + + def test_can_view_other_domain_contacts + secondary_contact = contacts(:jane) + + visit registrant_domain_path(@domain) + assert_text secondary_contact.name + click_link secondary_contact.name + assert_text @domain.name + assert_text secondary_contact.email + end +end From 18ce8534206f9bfc28c5416fa6512235de793e6f Mon Sep 17 00:00:00 2001 From: Maciej Szlosarczyk Date: Thu, 14 May 2020 14:33:38 +0300 Subject: [PATCH 06/20] Simple implementation of retained domains API endpoint This needed a new name, there are several classes of database object to be included in that endpoint. Currently, there is one list ordered by name, with each object containing status and ascii name for convenience. Can be converted to multiple fields (reserved and blocked separately). Also contains total count. Includes CORS preflight which seems to be a known problem for Rails in the past. --- .../repp/v1/retained_domains_controller.rb | 11 ++++ app/models/retained_domains.rb | 51 +++++++++++++++++++ config/routes.rb | 12 +++++ .../integration/repp/retained_domains_test.rb | 39 ++++++++++++++ 4 files changed, 113 insertions(+) create mode 100644 app/controllers/repp/v1/retained_domains_controller.rb create mode 100644 app/models/retained_domains.rb create mode 100644 test/integration/repp/retained_domains_test.rb diff --git a/app/controllers/repp/v1/retained_domains_controller.rb b/app/controllers/repp/v1/retained_domains_controller.rb new file mode 100644 index 000000000..e28bc531f --- /dev/null +++ b/app/controllers/repp/v1/retained_domains_controller.rb @@ -0,0 +1,11 @@ +module Repp + module V1 + class RetainedDomainsController < ActionController::API + def index + domains = RetainedDomains.new + + render json: { count: domains.count, domains: domains.to_jsonable } + end + end + end +end diff --git a/app/models/retained_domains.rb b/app/models/retained_domains.rb new file mode 100644 index 000000000..b0f077919 --- /dev/null +++ b/app/models/retained_domains.rb @@ -0,0 +1,51 @@ +# Hiding the queries behind its own class will allow us to include disputed or +# auctioned domains without meddling up with controller logic. +class RetainedDomains + RESERVED = 'reserved'.freeze + BLOCKED = 'blocked'.freeze + + attr_reader :domains + + def initialize + @domains = gather_domains + end + + def gather_domains + blocked_domains = BlockedDomain.order(name: :desc).all + reserved_domains = ReservedDomain.order(name: :desc).all + + domains = blocked_domains.to_a.union(reserved_domains.to_a) + + domains.sort_by(&:name) + end + + def to_jsonable + domains.map { |el| domain_to_json(el) } + end + + def domain_to_json(domain) + # Smelly, but ActiveRecord objects are weird and do not respond + # to usual syntax: + # case a + # when Array then "foo" + # when Hash then "bar" + # else "baz" + # end + status = case domain.class.to_s + when 'ReservedDomain' then RESERVED + when 'BlockedDomain' then BLOCKED + end + + punycode = SimpleIDN.to_ascii(domain.name) + + { + name: domain.name, + status: status, + punycode_name: punycode + } + end + + def count + domains.count + end +end diff --git a/config/routes.rb b/config/routes.rb index 53d78dfa9..a8b78a1f9 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -39,6 +39,18 @@ Rails.application.routes.draw do mount Repp::API => '/' + namespace :repp do + namespace :v1 do + resources :retained_domains, only: %i[index] + end + end + + match 'repp/v1/retained_domains', + controller: 'api/cors', + action: 'cors_preflight_check', + via: [:options], + as: 'repp_cors_preflight_check' + namespace :api do namespace :v1 do namespace :registrant do diff --git a/test/integration/repp/retained_domains_test.rb b/test/integration/repp/retained_domains_test.rb new file mode 100644 index 000000000..c30af001e --- /dev/null +++ b/test/integration/repp/retained_domains_test.rb @@ -0,0 +1,39 @@ +require 'test_helper' + +class ReppV1RetainedDomainsTest < ActionDispatch::IntegrationTest + # Uses magical fixtures, will fail once fixtures inside are changed: + # test/fixtures/blocked_domains.yml + # test/fixtures/reserved_domains.yml + + def test_get_index + get repp_v1_retained_domains_path + response_json = JSON.parse(response.body, symbolize_names: true) + + assert response_json[:count] == 3 + + expected_objects = [{ name: 'blocked.test', + status: 'blocked', + punycode_name: 'blocked.test' }, + { name: 'blockedäöüõ.test', + status: 'blocked', + punycode_name: 'xn--blocked-cxa7mj0e.test' }, + { name: 'reserved.test', + status: 'reserved', + punycode_name: 'reserved.test' }] + + assert_equal response_json[:domains], expected_objects + end + + def test_cors_preflight + process :options, repp_v1_retained_domains_path, headers: { 'Origin' => 'https://example.com' } + + assert_equal('https://example.com', response.headers['Access-Control-Allow-Origin']) + assert_equal('POST, GET, PUT, PATCH, DELETE, OPTIONS', + response.headers['Access-Control-Allow-Methods']) + assert_equal('Origin, Content-Type, Accept, Authorization, Token, Auth-Token, Email, ' \ + 'X-User-Token, X-User-Email', + response.headers['Access-Control-Allow-Headers']) + assert_equal('3600', response.headers['Access-Control-Max-Age']) + assert_equal('', response.body) + end +end From 88a3a2ebac27536a035d762122adfa5efe90b5ba Mon Sep 17 00:00:00 2001 From: Maciej Szlosarczyk Date: Thu, 14 May 2020 14:59:17 +0300 Subject: [PATCH 07/20] Make Rubocop less whiny about things --- app/models/retained_domains.rb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/models/retained_domains.rb b/app/models/retained_domains.rb index b0f077919..98fa9f5ff 100644 --- a/app/models/retained_domains.rb +++ b/app/models/retained_domains.rb @@ -41,11 +41,9 @@ class RetainedDomains { name: domain.name, status: status, - punycode_name: punycode + punycode_name: punycode, } end - def count - domains.count - end + delegate :count, to: :domains end From 960e4084e3225287a52bc5f1a504e1439b7925e3 Mon Sep 17 00:00:00 2001 From: Maciej Szlosarczyk Date: Thu, 14 May 2020 15:26:33 +0300 Subject: [PATCH 08/20] Properly match against class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Turns out I was wrong 😅 --- app/models/retained_domains.rb | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/app/models/retained_domains.rb b/app/models/retained_domains.rb index 98fa9f5ff..e821adbc9 100644 --- a/app/models/retained_domains.rb +++ b/app/models/retained_domains.rb @@ -24,16 +24,9 @@ class RetainedDomains end def domain_to_json(domain) - # Smelly, but ActiveRecord objects are weird and do not respond - # to usual syntax: - # case a - # when Array then "foo" - # when Hash then "bar" - # else "baz" - # end - status = case domain.class.to_s - when 'ReservedDomain' then RESERVED - when 'BlockedDomain' then BLOCKED + status = case domain + when ReservedDomain then RESERVED + when BlockedDomain then BLOCKED end punycode = SimpleIDN.to_ascii(domain.name) From 5f645d7370667c33e0c3e40c9cde0d0fd4762361 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20V=C3=B5hmar?= Date: Thu, 14 May 2020 18:56:20 +0300 Subject: [PATCH 09/20] Update CHANGELOG.md fixed revoking of certificates --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8ca16670..ae71b2adf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +14.05.2020 +* Deleted certificates are now revoked first [#952](https://github.com/internetee/registry/issues/952) + 11.05.2020 * Auction process due dates are now available over whois and rest-whois [#1201](https://github.com/internetee/registry/issues/1201) From 6e5a97ad4db836f41a0a695024e0b7e95321f606 Mon Sep 17 00:00:00 2001 From: Maciej Szlosarczyk Date: Fri, 15 May 2020 14:43:18 +0300 Subject: [PATCH 10/20] Add handling of type filters and handling of ETags Add API documentation and test cases around ETags for the API. --- .../repp/v1/retained_domains_controller.rb | 6 +- app/models/retained_domains.rb | 47 +++++++-- doc/repp/v1/retained_domains.md | 96 +++++++++++++++++++ doc/repp_doc.md | 17 ++-- .../integration/repp/retained_domains_test.rb | 35 +++++++ 5 files changed, 182 insertions(+), 19 deletions(-) create mode 100644 doc/repp/v1/retained_domains.md diff --git a/app/controllers/repp/v1/retained_domains_controller.rb b/app/controllers/repp/v1/retained_domains_controller.rb index e28bc531f..c1bb458e9 100644 --- a/app/controllers/repp/v1/retained_domains_controller.rb +++ b/app/controllers/repp/v1/retained_domains_controller.rb @@ -2,10 +2,14 @@ module Repp module V1 class RetainedDomainsController < ActionController::API def index - domains = RetainedDomains.new + domains = RetainedDomains.new(query_params) render json: { count: domains.count, domains: domains.to_jsonable } end + + def query_params + params.permit(:type) + end end end end diff --git a/app/models/retained_domains.rb b/app/models/retained_domains.rb index e821adbc9..ea76710ee 100644 --- a/app/models/retained_domains.rb +++ b/app/models/retained_domains.rb @@ -4,26 +4,55 @@ class RetainedDomains RESERVED = 'reserved'.freeze BLOCKED = 'blocked'.freeze - attr_reader :domains + attr_reader :domains, + :type - def initialize + def initialize(params) + @type = establish_type(params) @domains = gather_domains end - def gather_domains - blocked_domains = BlockedDomain.order(name: :desc).all - reserved_domains = ReservedDomain.order(name: :desc).all + delegate :count, to: :domains + def to_jsonable + domains.map { |el| domain_to_jsonable(el) } + end + + private + + def establish_type(params) + type = params[:type] + + case type + when 'reserved' then :reserved + when 'blocked' then :blocked + else :all + end + end + + def gather_domains domains = blocked_domains.to_a.union(reserved_domains.to_a) domains.sort_by(&:name) end - def to_jsonable - domains.map { |el| domain_to_json(el) } + def blocked_domains + if %i[all blocked].include?(type) + BlockedDomain.order(name: :desc).all + else + [] + end end - def domain_to_json(domain) + def reserved_domains + if %i[all reserved].include?(type) + ReservedDomain.order(name: :desc).all + else + [] + end + end + + def domain_to_jsonable(domain) status = case domain when ReservedDomain then RESERVED when BlockedDomain then BLOCKED @@ -37,6 +66,4 @@ class RetainedDomains punycode_name: punycode, } end - - delegate :count, to: :domains end diff --git a/doc/repp/v1/retained_domains.md b/doc/repp/v1/retained_domains.md new file mode 100644 index 000000000..bc930f435 --- /dev/null +++ b/doc/repp/v1/retained_domains.md @@ -0,0 +1,96 @@ +## GET /repp/v1/retained_domains + +Return a list of reserved and blocked domains, along with total count. You can +filter them by type of the domain, which can be either reserved or blocked. + +In contrast with other endpoints in REPP, this one is publicly available for +anyone without authentication. + +#### Parameters + +| Field name | Required | Type | Allowed values | Description | +| ---------- | -------- | ---- | -------------- | ----------- | +| type | false | string | ["reserved", "blocked"] | Type of domains to show | + + +#### Request + +``` +GET /repp/v1/retained_domains?type=reserved HTTP/1.1 +Accept: application/json +User-Agent: curl/7.64.1 +``` + +#### Response + +``` +HTTP/1.1 200 OK +Date: Fri, 15 May 2020 11:30:07 GMT +Content-Type: application/json; charset=utf-8 +ETag: W/"a905b531243a6b0be42beb9d6ce60619" +Cache-Control: max-age=0, private, must-revalidate +Transfer-Encoding: chunked + +{ + "count": 1, + "domains": [ + { + "name": "reserved.test", + "status": "reserved", + "punycode_name": "reserved.test" + } + ] +} +``` + +After you have made the first request, you can save the ETag header, and +send it as If-None-Match in the subsequent request for cache validation. +Due to the fact that the lists are not changing frequently and are quite long, +it is recommended that you take advantage of ETag cache. + +ETag key values depend on the request parameters. A request for only blocked +domains returns different cache key than request for all domains. + +### Cache Request + +``` +GET /repp/v1/retained_domains?type=reserved HTTP/1.1 +Accept: application/json +User-Agent: curl/7.64.1 +If-None-Match: W/"a905b531243a6b0be42beb9d6ce60619" +``` + +#### Cache hit response + +Response with no body and status 304 is sent in case the list have not changed. + +``` +HTTP/1.1 304 Not Modified +Date: Fri, 15 May 2020 11:34:25 GMT +ETag: W/"a905b531243a6b0be42beb9d6ce60619" +Cache-Control: max-age=0, private, must-revalidate +``` + +#### Cache miss response + +Standard 200 response is sent when the list have changed since last requested. + + +``` +HTTP/1.1 200 OK +Date: Fri, 15 May 2020 11:30:07 GMT +Content-Type: application/json; charset=utf-8 +ETag: W/"a905b531243a6b0be42beb9d6ce60619" +Transfer-Encoding: chunked + +{ + "count": 1, + "domains": [ + { + "name": "reserved.test", + "status": "reserved", + "punycode_name": "reserved.test" + } + ] +} +``` diff --git a/doc/repp_doc.md b/doc/repp_doc.md index f01484fc3..0d7cb55f8 100644 --- a/doc/repp_doc.md +++ b/doc/repp_doc.md @@ -1,7 +1,7 @@ # REPP integration specification -REPP uses HTTP/1.1 protocol (http://tools.ietf.org/html/rfc2616) and -Basic Authentication (http://tools.ietf.org/html/rfc2617#section-2) using +REPP uses HTTP/1.1 protocol (http://tools.ietf.org/html/rfc2616) and +Basic Authentication (http://tools.ietf.org/html/rfc2617#section-2) using Secure Transport (https://tools.ietf.org/html/rfc5246) with certificate and key (https://tools.ietf.org/html/rfc5280). Credentials and certificate are issued by EIS (in an exchange for desired API username, CSR and IP). @@ -10,13 +10,14 @@ To quickly test the API, use curl: curl -q -k --cert user.crt.pem --key user.key.pem https://TBA/repp/v1/accounts/balance -u username:password -Test API endpoint: https://testepp.internet.ee/repp/v1 +Test API endpoint: https://testepp.internet.ee/repp/v1 Production API endpoint: TBA Main communication specification through Restful EPP (REPP): -[Contact related functions](repp/v1/contact.md) -[Domain related functions](repp/v1/domain.md) -[Domain transfers](repp/v1/domain_transfers.md) -[Account related functions](repp/v1/account.md) -[Nameservers](repp/v1/nameservers.md) +[Contact related functions](repp/v1/contact.md) +[Domain related functions](repp/v1/domain.md) +[Domain transfers](repp/v1/domain_transfers.md) +[Account related functions](repp/v1/account.md) +[Nameservers](repp/v1/nameservers.md) +[Retained domains](repp/v1/retained_domains.md) diff --git a/test/integration/repp/retained_domains_test.rb b/test/integration/repp/retained_domains_test.rb index c30af001e..dc3a9fd0f 100644 --- a/test/integration/repp/retained_domains_test.rb +++ b/test/integration/repp/retained_domains_test.rb @@ -24,6 +24,41 @@ class ReppV1RetainedDomainsTest < ActionDispatch::IntegrationTest assert_equal response_json[:domains], expected_objects end + def test_get_index_with_type_parameter + get repp_v1_retained_domains_path({ 'type' => 'reserved' }) + response_json = JSON.parse(response.body, symbolize_names: true) + + assert response_json[:count] == 1 + + expected_objects = [{ name: 'reserved.test', + status: 'reserved', + punycode_name: 'reserved.test' }] + + assert_equal response_json[:domains], expected_objects + end + + def test_etags_cache + get repp_v1_retained_domains_path({ 'type' => 'reserved' }) + etag = response.headers['ETag'] + + get repp_v1_retained_domains_path({ 'type' => 'reserved' }), + headers: { 'If-None-Match' => etag } + + assert_equal response.status, 304 + assert_equal response.body, '' + end + + def test_etags_cache_valid_for_type_only + get repp_v1_retained_domains_path({ 'type' => 'blocked' }) + etag = response.headers['ETag'] + + get repp_v1_retained_domains_path, headers: { 'If-None-Match' => etag } + + assert_equal response.status, 200 + response_json = JSON.parse(response.body, symbolize_names: true) + assert response_json[:count] == 3 + end + def test_cors_preflight process :options, repp_v1_retained_domains_path, headers: { 'Origin' => 'https://example.com' } From 56e3784aa4245984219f5b2a363ef2a7a85dc704 Mon Sep 17 00:00:00 2001 From: Maciej Szlosarczyk Date: Fri, 15 May 2020 14:52:11 +0300 Subject: [PATCH 11/20] Use constants instead of strings --- app/models/retained_domains.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/retained_domains.rb b/app/models/retained_domains.rb index ea76710ee..b26bbab40 100644 --- a/app/models/retained_domains.rb +++ b/app/models/retained_domains.rb @@ -24,8 +24,8 @@ class RetainedDomains type = params[:type] case type - when 'reserved' then :reserved - when 'blocked' then :blocked + when RESERVED then :reserved + when BLOCKED then :blocked else :all end end From 09b4575a7137a412b1f8548aaf4da1bf4bd560e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20V=C3=B5hmar?= Date: Mon, 18 May 2020 12:46:52 +0300 Subject: [PATCH 12/20] Update retained_domains.md --- doc/repp/v1/retained_domains.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/repp/v1/retained_domains.md b/doc/repp/v1/retained_domains.md index bc930f435..4209b0f58 100644 --- a/doc/repp/v1/retained_domains.md +++ b/doc/repp/v1/retained_domains.md @@ -73,7 +73,7 @@ Cache-Control: max-age=0, private, must-revalidate #### Cache miss response -Standard 200 response is sent when the list have changed since last requested. +Standard 200 response (with the current complete list) is sent when the list have changed since last requested. ``` From 82109c506c11acff58fae26b2ffe4a8fe059cbc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20V=C3=B5hmar?= Date: Mon, 18 May 2020 14:57:29 +0300 Subject: [PATCH 13/20] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae71b2adf..767f7e85f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +18.05.2020 +* REPP returns list of reserved and blocked domains [#1569](https://github.com/internetee/registry/issues/1569) + 14.05.2020 * Deleted certificates are now revoked first [#952](https://github.com/internetee/registry/issues/952) From 572510d7b00e4cfb9796e7619f070b5fbe27b106 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl=20Erik=20=C3=95unapuu?= Date: Fri, 15 May 2020 13:27:33 +0300 Subject: [PATCH 14/20] Find contact/registrar directly via domain or via relations --- app/controllers/registrant/contacts_controller.rb | 9 ++++++++- app/models/contact.rb | 15 +++++++++------ app/models/registrant_user.rb | 2 +- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/app/controllers/registrant/contacts_controller.rb b/app/controllers/registrant/contacts_controller.rb index 1ad403248..136596ede 100644 --- a/app/controllers/registrant/contacts_controller.rb +++ b/app/controllers/registrant/contacts_controller.rb @@ -3,9 +3,9 @@ class Registrant::ContactsController < RegistrantController helper_method :fax_enabled? helper_method :domain_filter_params skip_authorization_check only: %i[edit update] + before_action :set_contact, only: [:show] def show - @contact = domain.contacts.find(params[:id]) @requester_contact = Contact.find_by(ident: current_registrant_user.ident).id authorize! :read, @contact end @@ -31,6 +31,13 @@ class Registrant::ContactsController < RegistrantController private + def set_contact + id = params[:id] + contact = domain.contacts.find_by(id: id) || current_user_contacts.find_by(id: id) + contact ||= Contact.find_by(id: id, ident: domain.registrant.ident) + @contact = contact + end + def domain current_user_domains.find(params[:domain_id]) end diff --git a/app/models/contact.rb b/app/models/contact.rb index 488e72335..2aeb761a8 100644 --- a/app/models/contact.rb +++ b/app/models/contact.rb @@ -415,7 +415,7 @@ class Contact < ApplicationRecord # if total is smaller than needed, the load more # we also need to sort by valid_to # todo: extract to drapper. Then we can remove Domain#roles - def all_domains(page: nil, per: nil, params:, requester:) + def all_domains(page: nil, per: nil, params:, requester: nil) filter_sql = qualified_domain_ids(params[:domain_filter]) # get sorting rules @@ -451,9 +451,12 @@ class Contact < ApplicationRecord end def qualified_domain_name_list(requester, filter_sql) - if requester - requester_domains = Contact.find(requester).domains - domains = requester_domains.where('domains.id IN (?)', filter_sql) + if requester != id + first_scope = Contact.find(requester).domains + second_scope = Contact.find(requester).registrant_domains + + domains = Domain.from("(#{first_scope.to_sql} UNION #{second_scope.to_sql}) as domains") + .where('domains.id IN (?)', filter_sql) else domains = Domain.where('domains.id IN (?)', filter_sql) end @@ -462,13 +465,13 @@ class Contact < ApplicationRecord end def qualified_domain_ids(domain_filter) - registrant_ids = Domain.select('id').where(registrant: id).pluck(:id) + registrant_ids = registrant_domains.pluck(:id) return registrant_ids if domain_filter == 'Registrant' if %w[AdminDomainContact TechDomainContact].include? domain_filter DomainContact.select('domain_id').where(contact_id: id, type: domain_filter) else - (DomainContact.select('domain_id').where(contact_id: id).pluck(:id) + + (DomainContact.select('domain_id').where(contact_id: id).pluck(:domain_id) + registrant_ids).uniq end end diff --git a/app/models/registrant_user.rb b/app/models/registrant_user.rb index 1e787b8b3..e7ce9cc3b 100644 --- a/app/models/registrant_user.rb +++ b/app/models/registrant_user.rb @@ -98,4 +98,4 @@ class RegistrantUser < User user end end -end \ No newline at end of file +end From bcd6f0bd71c30b571cc9039f7019676c733bc5bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl=20Erik=20=C3=95unapuu?= Date: Thu, 21 May 2020 11:57:22 +0300 Subject: [PATCH 15/20] Find requester's tied domains via RegistrantUser --- app/models/contact.rb | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/app/models/contact.rb b/app/models/contact.rb index 2aeb761a8..efee4926e 100644 --- a/app/models/contact.rb +++ b/app/models/contact.rb @@ -451,17 +451,16 @@ class Contact < ApplicationRecord end def qualified_domain_name_list(requester, filter_sql) - if requester != id - first_scope = Contact.find(requester).domains - second_scope = Contact.find(requester).registrant_domains + return Domain.where('domains.id IN (?)', filter_sql) unless requester != id - domains = Domain.from("(#{first_scope.to_sql} UNION #{second_scope.to_sql}) as domains") - .where('domains.id IN (?)', filter_sql) - else - domains = Domain.where('domains.id IN (?)', filter_sql) + requester = Contact.find_by(id: requester) + registrant_user = RegistrantUser.find_or_initialize_by(registrant_ident: + "#{requester.ident_country_code}-#{requester.ident}") + begin + registrant_user.domains.where('domains.id IN (?)', filter_sql) + rescue CompanyRegister::NotAvailableError + registrant_user.direct_domains.where('domains.id IN (?)', filter_sql) end - - domains end def qualified_domain_ids(domain_filter) From f27f2f365f476b13c35522bd52c2a597518e1e29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl=20Erik=20=C3=95unapuu?= Date: Thu, 21 May 2020 14:15:00 +0300 Subject: [PATCH 16/20] Do not use requester ID when querying all_domains() via Registrar --- app/models/contact.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/contact.rb b/app/models/contact.rb index efee4926e..58d8b8c60 100644 --- a/app/models/contact.rb +++ b/app/models/contact.rb @@ -451,7 +451,7 @@ class Contact < ApplicationRecord end def qualified_domain_name_list(requester, filter_sql) - return Domain.where('domains.id IN (?)', filter_sql) unless requester != id + return Domain.where('domains.id IN (?)', filter_sql) if requester.nil? requester = Contact.find_by(id: requester) registrant_user = RegistrantUser.find_or_initialize_by(registrant_ident: From fda3d346b363bf15d223ec563932aad6babe24df Mon Sep 17 00:00:00 2001 From: Maciej Szlosarczyk Date: Thu, 21 May 2020 14:12:37 +0300 Subject: [PATCH 17/20] Add auctions endpoint to REPP --- .../repp/v1/auctions_controller.rb | 23 +++++++++++ config/routes.rb | 3 +- doc/repp/v1/auctions.md | 39 +++++++++++++++++++ doc/repp_doc.md | 1 + test/integration/repp/auctions_test.rb | 23 +++++++++++ 5 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 app/controllers/repp/v1/auctions_controller.rb create mode 100644 doc/repp/v1/auctions.md create mode 100644 test/integration/repp/auctions_test.rb diff --git a/app/controllers/repp/v1/auctions_controller.rb b/app/controllers/repp/v1/auctions_controller.rb new file mode 100644 index 000000000..4a5265d13 --- /dev/null +++ b/app/controllers/repp/v1/auctions_controller.rb @@ -0,0 +1,23 @@ +module Repp + module V1 + class AuctionsController < ActionController::API + def index + auctions = Auction.started + + render json: { count: auctions.count, + auctions: auctions_to_json(auctions) } + end + + private + + def auctions_to_json(auctions) + auctions.map do |e| + { + domain_name: e.domain, + punycode_domain_name: SimpleIDN.to_ascii(e.domain), + } + end + end + end + end +end diff --git a/config/routes.rb b/config/routes.rb index a8b78a1f9..17045edbf 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -41,11 +41,12 @@ Rails.application.routes.draw do namespace :repp do namespace :v1 do + resources :auctions, only: %i[index] resources :retained_domains, only: %i[index] end end - match 'repp/v1/retained_domains', + match 'repp/v1/*all', controller: 'api/cors', action: 'cors_preflight_check', via: [:options], diff --git a/doc/repp/v1/auctions.md b/doc/repp/v1/auctions.md new file mode 100644 index 000000000..727e6712e --- /dev/null +++ b/doc/repp/v1/auctions.md @@ -0,0 +1,39 @@ +## GET /repp/v1/auctions + +Return a list of auctions currently in progress. The list of domains changes +every day. + +In contrast with other endpoints in REPP, this one is publicly available for +anyone without authentication. + +#### Request + +``` +GET /repp/v1/auctions HTTP/1.1 +Host: registry.test +User-Agent: curl/7.64.1 +Accept: */* +``` + +#### Response + +``` +HTTP/1.1 200 OK +Date: Thu, 21 May 2020 10:39:45 GMT +Content-Type: application/json; charset=utf-8 +ETag: W/"217bd9ee4dfbb332172a1baf80ee0ba9" +Cache-Control: max-age=0, private, must-revalidate +X-Request-Id: a26b6801-bf3f-4922-b0db-3b081bacb130 +X-Runtime: 1.481174 +Transfer-Encoding: chunked + +{ + "count":1, + "auctions": [ + { + "domain_name": "auctionäöüõ.test", + "punycode_domain_name": "xn--auction-cxa7mj0e.test" + } + ] +} +``` diff --git a/doc/repp_doc.md b/doc/repp_doc.md index 0d7cb55f8..1ffbf669c 100644 --- a/doc/repp_doc.md +++ b/doc/repp_doc.md @@ -21,3 +21,4 @@ Main communication specification through Restful EPP (REPP): [Account related functions](repp/v1/account.md) [Nameservers](repp/v1/nameservers.md) [Retained domains](repp/v1/retained_domains.md) +[Auctions](repp/v1/auctions.md) diff --git a/test/integration/repp/auctions_test.rb b/test/integration/repp/auctions_test.rb new file mode 100644 index 000000000..145e5d17a --- /dev/null +++ b/test/integration/repp/auctions_test.rb @@ -0,0 +1,23 @@ +require 'test_helper' + +class ReppV1AuctionsTest < ActionDispatch::IntegrationTest + setup do + @auction = auctions(:one) + + @auction.update!(uuid: '1b3ee442-e8fe-4922-9492-8fcb9dccc69c', + domain: 'auction.test', + status: Auction.statuses[:started]) + end + + def test_get_index + get repp_v1_auctions_path + response_json = JSON.parse(response.body, symbolize_names: true) + + assert response_json[:count] == 1 + + expected_response = [{ domain_name: @auction.domain, + punycode_domain_name: @auction.domain }] + + assert_equal expected_response, response_json[:auctions] + end +end From bb1bd88a1764890f7dced49df37c107356de1945 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20V=C3=B5hmar?= Date: Thu, 21 May 2020 14:35:26 +0300 Subject: [PATCH 18/20] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 767f7e85f..69da0f0a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +21.05.2020 +* Fixed contact view access bug in registrant [#1527](https://github.com/internetee/registry/pull/1527) + 18.05.2020 * REPP returns list of reserved and blocked domains [#1569](https://github.com/internetee/registry/issues/1569) From 97879a40bedeef04fc48873482998e2560230ece Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20V=C3=B5hmar?= Date: Thu, 21 May 2020 15:02:42 +0300 Subject: [PATCH 19/20] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69da0f0a5..f40854eb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ 21.05.2020 * Fixed contact view access bug in registrant [#1527](https://github.com/internetee/registry/pull/1527) +* REPP returns list of domains currently at auction [#1583](https://github.com/internetee/registry/pull/1583) 18.05.2020 * REPP returns list of reserved and blocked domains [#1569](https://github.com/internetee/registry/issues/1569) From da5639ea4a0bbe4d407565ec3d1b9d214daa1f75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20V=C3=B5hmar?= Date: Thu, 21 May 2020 15:06:48 +0300 Subject: [PATCH 20/20] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f40854eb4..e3078e604 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ 21.05.2020 * Fixed contact view access bug in registrant [#1527](https://github.com/internetee/registry/pull/1527) -* REPP returns list of domains currently at auction [#1583](https://github.com/internetee/registry/pull/1583) +* REPP returns list of domains currently at auction [#1582](https://github.com/internetee/registry/pull/1582) 18.05.2020 * REPP returns list of reserved and blocked domains [#1569](https://github.com/internetee/registry/issues/1569)