Merge remote-tracking branch 'origin/master' into 269-dispute-list

This commit is contained in:
Karl Erik Õunapuu 2020-05-21 17:43:57 +03:00
commit 7ef15a5bf1
15 changed files with 448 additions and 36 deletions

View file

@ -1,3 +1,13 @@
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 [#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)
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)

View file

@ -3,9 +3,10 @@ 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 = current_user_contacts.find(params[:id])
@requester_contact = Contact.find_by(ident: current_registrant_user.ident).id
authorize! :read, @contact
end
@ -30,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

View file

@ -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

View file

@ -0,0 +1,15 @@
module Repp
module V1
class RetainedDomainsController < ActionController::API
def index
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

View file

@ -415,45 +415,66 @@ 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:)
# 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
def all_domains(page: nil, per: nil, params:, requester: nil)
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
domains = Domain.where("domains.id IN (#{filter_sql})")
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)
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:
"#{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
end
def qualified_domain_ids(domain_filter)
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(:domain_id) +
registrant_ids).uniq
end
end
def update_prohibited?
(statuses & [
CLIENT_UPDATE_PROHIBITED,

View file

@ -98,4 +98,4 @@ class RegistrantUser < User
user
end
end
end
end

View file

@ -0,0 +1,69 @@
# 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,
:type
def initialize(params)
@type = establish_type(params)
@domains = gather_domains
end
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 blocked_domains
if %i[all blocked].include?(type)
BlockedDomain.order(name: :desc).all
else
[]
end
end
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
end
punycode = SimpleIDN.to_ascii(domain.name)
{
name: domain.name,
status: status,
punycode_name: punycode,
}
end
end

View file

@ -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) %>
<div class="panel panel-default">
<div class="panel-heading">

View file

@ -39,6 +39,19 @@ Rails.application.routes.draw do
mount Repp::API => '/'
namespace :repp do
namespace :v1 do
resources :auctions, only: %i[index]
resources :retained_domains, only: %i[index]
end
end
match 'repp/v1/*all',
controller: 'api/cors',
action: 'cors_preflight_check',
via: [:options],
as: 'repp_cors_preflight_check'
namespace :api do
namespace :v1 do
namespace :registrant do

39
doc/repp/v1/auctions.md Normal file
View file

@ -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"
}
]
}
```

View file

@ -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 (with the current complete list) 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"
}
]
}
```

View file

@ -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,15 @@ 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)
[Auctions](repp/v1/auctions.md)

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,74 @@
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_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' }
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