Merge branch 'master' into refactor-devise-integration

This commit is contained in:
Artur Beljajev 2018-08-11 14:58:54 +03:00
commit 60f451afcd
10 changed files with 815 additions and 4 deletions

View file

@ -0,0 +1,57 @@
module Api
module V1
module Registrant
class ContactsController < BaseController
before_action :set_contacts_pool
def index
limit = params[:limit] || 200
offset = params[:offset] || 0
if limit.to_i > 200 || limit.to_i < 1
render(json: { errors: [{ limit: ['parameter is out of range'] }] },
status: :bad_request) && return
end
if offset.to_i.negative?
render(json: { errors: [{ offset: ['parameter is out of range'] }] },
status: :bad_request) && return
end
@contacts = @contacts_pool.limit(limit).offset(offset)
render json: @contacts
end
def show
@contact = @contacts_pool.find_by(uuid: params[:uuid])
if @contact
render json: @contact
else
render json: { errors: [{ base: ['Contact not found'] }] }, status: :not_found
end
end
private
def set_contacts_pool
country_code, ident = current_user.registrant_ident.to_s.split '-'
associated_domain_ids = begin
BusinessRegistryCache.fetch_by_ident_and_cc(ident, country_code).associated_domain_ids
end
available_contacts_ids = begin
DomainContact.where(domain_id: associated_domain_ids).pluck(:contact_id) |
Domain.where(id: associated_domain_ids).pluck(:registrant_id)
end
@contacts_pool = Contact.where(id: available_contacts_ids)
rescue Soap::Arireg::NotAvailableError => error
Rails.logger.fatal("[EXCEPTION] #{error}")
render json: { errors: [{ base: ['Business Registry not available'] }] },
status: :service_unavailable and return
end
end
end
end
end

View file

@ -1,5 +1,3 @@
require 'rails5_api_controller_backport'
module Api
module V1
module Registrant

View file

@ -25,4 +25,4 @@ class Registrant::ContactsController < RegistrantController
BusinessRegistryCache.fetch_by_ident_and_cc(ident, ident_cc).associated_domain_ids
end
end
end
end

View file

@ -23,7 +23,8 @@ Rails.application.routes.draw do
namespace :registrant do
post 'auth/eid', to: 'auth#eid'
resources :domains, only: [:index, :show], param: :uuid
resources :domains, only: %i[index show], param: :uuid
resources :contacts, only: %i[index show], param: :uuid
end
end
end

11
doc/registrant-api.md Normal file
View file

@ -0,0 +1,11 @@
# Registrant API integration specification
Test API endpoint: TBA
Production API endpoint: TBA
Main communication specification through Registrant API:
[Authentication](registrant-api/v1/authentication.md)
[Domains](registrant-api/v1/domain.md)
[Domain Lock](registrant-api/v1/domain_lock.md)
[Contacts](registrant-api/v1/contact.md)

View file

@ -0,0 +1,109 @@
# Authentication
## Authenticating with mobileID or ID-card
For specified partners the API allows for use of data from mobile ID for
authentication. API client should perform authentication with eID according to
the approriate documentation, and then pass on values from the webserver's
certificate to the API server.
## POST /repp/v1/registrant/auth/eid
Returns a bearer token to be used for further API requests. Tokens are valid for 2 hours since their creation.
#### Paramaters
Values in brackets represent values that come from the id card certificate.
| Field name | Required | Type | Allowed values | Description |
| ----------------- | -------- | ---- | -------------- | ----------- |
| ident | true | String | | Identity code of the user (`serialNumber`) |
| first_name | true | String | | Name of the customer (`GN`) |
| last_name | true | String | | Name of the customer (`SN`) |
#### Request
```
POST /repp/v1/auth/token HTTP/1.1
Accept: application/json
Content-type: application/json
{
"ident": "30110100103",
"first_name": "Jan",
"last_name": "Tamm",
}
```
#### Response
```
HTTP/1.1 201
Content-Type: application.json
{
"access_token": "<SOME TOKEN>",
"expires_at": "2018-07-13 11:30:51 UTC",
"type": "Bearer"
}
```
## POST /repp/v1/auth/username -- NOT IMPLEMENTED
#### Paramaters
Values in brackets represent values that come from the id card certificate
| Field name | Required | Type | Allowed values | Description |
| ----------------- | -------- | ---- | -------------- | ----------- |
| username | true | String | Username as provided by the user | |
| password | true | String | Password as provided by the user | |
#### Request
```
POST /repp/v1/auth/token HTTP/1.1
Accept: application/json
Content-type: application/json
```
#### Response
```
HTTP/1.1 201
Content-Type: application.json
{
"access_token": "<SOME TOKEN>",
"expires_at": "2018-07-13 11:30:51 UTC",
"type": "Bearer"
}
```
## Implementation notes:
We do not need to store the session data at all, instead we can levarage AES encryption and use
Rails secret as the key. General approximation:
```ruby
class AuthenticationToken
def initialize(secret = Rails.application.config.secret_key_base, values = {})
end
def create_token_hash
data = values.to_s
cipher = OpenSSL::Cipher::AES.new(256, :CBC)
cipher.encrypt
encrypted = cipher.update(data) + cipher.final
base64_encoded = Base64.encode64(encrypted)
{
token: base64_encoded,
expires_in = values[:expires_in]
type: "Bearer"
}
end
end
```

View file

@ -0,0 +1,194 @@
## GET /repp/v1/registrant/contacts
Returns contacts of the current registrar.
#### Parameters
| Field name | Required | Type | Allowed values | Description |
| ---------- | -------- | ---- | -------------- | ----------- |
| limit | false | Integer | [1..200] | How many contacts to show |
| offset | false | Integer | | Contact number to start at |
#### Request
```
GET /repp/v1/registrant/contacts?limit=1 HTTP/1.1
Accept: application/json
Authorization: Bearer Z2l0bGFiOmdoeXQ5ZTRmdQ==
Content-Type: application/json
```
#### Response
```
HTTP/1.1 200
Content-Type: application/json
{
"contacts": [
{
"uuid": "84c62f3d-e56f-40fa-9ca4-dc0137778949",
"domain_names": ["example.com"],
"code": "REGISTRAR2:SH022086480",
"phone": "+372.12345678",
"email": "hoyt@deckowbechtelar.net",
"fax": null,
"created_at": "2015-09-09T09:11:14.130Z",
"updated_at": "2015-09-09T09:11:14.130Z",
"ident": "37605030299",
"ident_type": "priv",
"auth_info": "password",
"name": "Karson Kessler0",
"org_name": null,
"registrar_id": 2,
"creator_str": null,
"updator_str": null,
"ident_country_code": "EE",
"city": "Tallinn",
"street": "Short street 11",
"zip": "11111",
"country_code": "EE",
"state": null,
"legacy_id": null,
"statuses": [
"ok"
],
"status_notes": {
}
}
],
"total_number_of_records": 2
}
```
## GET /repp/v1/registrant/contacts/$UUID
Returns contacts of the current registrar.
#### Request
```
GET /repp/v1/registrant/contacts/84c62f3d-e56f-40fa-9ca4-dc0137778949 HTTP/1.1
Accept: application/json
Authorization: Bearer Z2l0bGFiOmdoeXQ5ZTRmdQ==
Content-Type: application/json
```
#### Response
```
HTTP/1.1 200
Content-Type: application/json
{
"uuid": "84c62f3d-e56f-40fa-9ca4-dc0137778949",
"domain_names": ["example.com"],
"code": "REGISTRAR2:SH022086480",
"phone": "+372.12345678",
"email": "hoyt@deckowbechtelar.net",
"fax": null,
"created_at": "2015-09-09T09:11:14.130Z",
"updated_at": "2015-09-09T09:11:14.130Z",
"ident": "37605030299",
"ident_type": "priv",
"auth_info": "password",
"name": "Karson Kessler0",
"org_name": null,
"registrar_id": 2,
"creator_str": null,
"updator_str": null,
"ident_country_code": "EE",
"city": "Tallinn",
"street": "Short street 11",
"zip": "11111",
"country_code": "EE",
"state": null,
"legacy_id": null,
"statuses": [
"ok"
],
"status_notes": {}
}
```
## PUT/PATCH /repp/v1/registrant/contacts/$UUID
Update contact details for a contact.
#### Parameters
| Field name | Required | Type | Allowed values | Description |
| ---- | --- | --- | --- | --- |
| email | false | String | | New email address |
| phone | false | String | | New phone number |
| fax | false | String | | New fax number |
| city | false | String | | New city name |
| street | false | String | | New street name |
| zip | false | String | | New zip code |
| country_code | false | String | | New country code in 2 letter format ('EE', 'LV') |
| state | false | String | | New state name |
#### Request
```
PUT /repp/v1/registrant/contacts/84c62f3d-e56f-40fa-9ca4-dc0137778949 HTTP/1.1
Authorization: Bearer Z2l0bGFiOmdoeXQ5ZTRmdQ==
Accept: application/json
Content-type: application/json
{
"email": "foo@bar.baz",
"phone": "+372.12345671",
"fax": "+372.12345672",
"city": "New City",
"street": "Main Street 123",
"zip": "22222",
"country_code": "LV",
"state": "New state"
}
```
#### Response on success
```
HTTP/1.1 200
Content-Type: application.json
{
"uuid": "84c62f3d-e56f-40fa-9ca4-dc0137778949",
"domain_names": ["example.com"],
"code": "REGISTRAR2:SH022086480",
"phone": "+372.12345671",
"email": "foo@bar.baz",
"fax": "+372.12345672",
"created_at": "2015-09-09T09:11:14.130Z",
"updated_at": "2018-09-09T09:11:14.130Z",
"ident": "37605030299",
"ident_type": "priv",
"auth_info": "password",
"name": "Karson Kessler0",
"org_name": null,
"registrar_id": 2,
"creator_str": null,
"updator_str": null,
"ident_country_code": "EE",
"city": "New City",
"street": "Main Street 123",
"zip": "22222",
"country_code": "LV",
"state": "New state"
"legacy_id": null,
"statuses": [
"ok"
],
"status_notes": {}
}
```
### Response on failure
```
HTTP/1.1 400
Content-Type: application.json
{
"errors": [
{ "phone": "Phone nr is invalid" }
]
}
```

View file

@ -0,0 +1,161 @@
# Domain related actions
## GET /repp/v1/registrant/domains
Returns domains of the current registrant.
#### Parameters
| Field name | Required | Type | Allowed values | Description |
| ---------- | -------- | ---- | -------------- | ----------- |
| limit | false | Integer | [1..200] | How many domains to show |
| offset | false | Integer | | Domain number to start at |
| details | false | String | ["true", "false"] | Whether to include details |
#### Request
```
GET repp/v1/registrant/domains?limit=1&details=true HTTP/1.1
Accept: application/json
Authorization: Bearer Z2l0bGFiOmdoeXQ5ZTRmdQ==
Content-Type: application/json
```
#### Response
```
HTTP/1.1 200
Content-Type: application/json
{
"domains": [
{
"uuid": "98d1083a-8863-4153-93e4-caee4a013535",
"name": "domain0.ee",
"registrar_id": 2,
"registered_at": "2015-09-09T09:11:14.861Z",
"status": null,
"valid_from": "2015-09-09T09:11:14.861Z",
"valid_to": "2016-09-09T09:11:14.861Z",
"registrant_id": 1,
"transfer_code": "98oiewslkfkd",
"created_at": "2015-09-09T09:11:14.861Z",
"updated_at": "2015-09-09T09:11:14.860Z",
"name_dirty": "domain0.ee",
"name_puny": "domain0.ee",
"period": 1,
"period_unit": "y",
"creator_str": null,
"updator_str": null,
"legacy_id": null,
"legacy_registrar_id": null,
"legacy_registrant_id": null,
"outzone_at": "2016-09-24T09:11:14.861Z",
"delete_at": "2016-10-24T09:11:14.861Z",
"registrant_verification_asked_at": null,
"registrant_verification_token": null,
"pending_json": {
},
"force_delete_at": null,
"statuses": [
"ok"
],
"reserved": false,
"status_notes": {
},
"statuses_backup": [
]
}
],
"total_number_of_records": 2
}
```
## GET repp/v1/registrant/domains
Returns domain names with offset.
#### Request
```
GET repp/v1/registrant/domains?offset=1 HTTP/1.1
Accept: application/json
Authorization: Bearer Z2l0bGFiOmdoeXQ5ZTRmdQ==
Content-Type: application/json
```
#### Response
```
HTTP/1.1 200
Content-Type: application/json
{
"domains": [
"domain1.ee"
],
"total_number_of_records": 2
}
```
## GET repp/v1/registrant/domains/$UUID
Returns a single domain object.
#### Request
```
GET repp/v1/registrant/domains/98d1083a-8863-4153-93e4-caee4a013535 HTTP/1.1
Accept: application/json
Authorization: Bearer Z2l0bGFiOmdoeXQ5ZTRmdQ==
Content-Type: application/json
```
#### Response for success
```
HTTP/1.1 200
Content-Type: application/json
{
"uuid": "98d1083a-8863-4153-93e4-caee4a013535",
"name": "domain0.ee",
"registrar_id": 2,
"registered_at": "2015-09-09T09:11:14.861Z",
"status": null,
"valid_from": "2015-09-09T09:11:14.861Z",
"valid_to": "2016-09-09T09:11:14.861Z",
"registrant_id": 1,
"transfer_code": "98oiewslkfkd",
"created_at": "2015-09-09T09:11:14.861Z",
"updated_at": "2015-09-09T09:11:14.860Z",
"name_dirty": "domain0.ee",
"name_puny": "domain0.ee",
"period": 1,
"period_unit": "y",
"creator_str": null,
"updator_str": null,
"legacy_id": null,
"legacy_registrar_id": null,
"legacy_registrant_id": null,
"outzone_at": "2016-09-24T09:11:14.861Z",
"delete_at": "2016-10-24T09:11:14.861Z",
"registrant_verification_asked_at": null,
"registrant_verification_token": null,
"pending_json": {},
"force_delete_at": null,
"statuses": [
"ok"
],
"reserved": false,
"status_notes": {},
"statuses_backup": []
}
```
#### Response for failure
```
HTTP/1.1 404
Content-Type: application/json
{ "errors": ["Domain not found"] }
```

View file

@ -0,0 +1,163 @@
# Domain locks
## POST repp/v1/registrant/domains/$UUID/registry_lock
Set a registry lock on a domain.
#### Request
```
POST repp/v1/registrant/domains/98d1083a-8863-4153-93e4-caee4a013535/registry_lock HTTP/1.1
Accept: application/json
Authorization: Bearer Z2l0bGFiOmdoeXQ5ZTRmdQ==
Content-Type: application/json
```
#### Response for success
```
HTTP/1.1 200
Content-Type: application/json
{
"uuid": "98d1083a-8863-4153-93e4-caee4a013535",
"name": "domain0.ee",
"registrar_id": 2,
"registered_at": "2015-09-09T09:11:14.861Z",
"status": null,
"valid_from": "2015-09-09T09:11:14.861Z",
"valid_to": "2016-09-09T09:11:14.861Z",
"registrant_id": 1,
"transfer_code": "98oiewslkfkd",
"created_at": "2015-09-09T09:11:14.861Z",
"updated_at": "2015-09-09T09:11:14.860Z",
"name_dirty": "domain0.ee",
"name_puny": "domain0.ee",
"period": 1,
"period_unit": "y",
"creator_str": null,
"updator_str": null,
"legacy_id": null,
"legacy_registrar_id": null,
"legacy_registrant_id": null,
"outzone_at": "2016-09-24T09:11:14.861Z",
"delete_at": "2016-10-24T09:11:14.861Z",
"registrant_verification_asked_at": null,
"registrant_verification_token": null,
"pending_json": {},
"force_delete_at": null,
"statuses": [
"serverUpdateProhibited",
"serverDeleteProhibited",
"serverTransferProhibited"
],
"reserved": false,
"status_notes": {},
"statuses_backup": []
}
```
#### Response for failure
```
HTTP/1.1 400
Content-Type: application/json
{
"errors": [
{ "base": "domain cannot be locked" }
]
}
```
```
HTTP/1.1 404
Content-Type: application/json
{
"errors": [
{ "base": "domain does not exist" }
]
}
```
## DELETE repp/v1/registrant/domains/$UUID/registry_lock
Remove a registry lock.
#### Request
```
DELETE repp/v1/registrant/domains/98d1083a-8863-4153-93e4-caee4a013535/registry_lock HTTP/1.1
Accept: application/json
Authorization: Bearer Z2l0bGFiOmdoeXQ5ZTRmdQ==
Content-Type: application/json
```
#### Response for success
```
HTTP/1.1 200
Content-Type: application/json
{
"uuid": "98d1083a-8863-4153-93e4-caee4a013535",
"name": "domain0.ee",
"registrar_id": 2,
"registered_at": "2015-09-09T09:11:14.861Z",
"status": null,
"valid_from": "2015-09-09T09:11:14.861Z",
"valid_to": "2016-09-09T09:11:14.861Z",
"registrant_id": 1,
"transfer_code": "98oiewslkfkd",
"created_at": "2015-09-09T09:11:14.861Z",
"updated_at": "2015-09-09T09:11:14.860Z",
"name_dirty": "domain0.ee",
"name_puny": "domain0.ee",
"period": 1,
"period_unit": "y",
"creator_str": null,
"updator_str": null,
"legacy_id": null,
"legacy_registrar_id": null,
"legacy_registrant_id": null,
"outzone_at": "2016-09-24T09:11:14.861Z",
"delete_at": "2016-10-24T09:11:14.861Z",
"registrant_verification_asked_at": null,
"registrant_verification_token": null,
"pending_json": {},
"force_delete_at": null,
"statuses": [
"ok"
],
"reserved": false,
"status_notes": {},
"statuses_backup": []
}
```
#### Response for failure
```
HTTP/1.1 400
Content-Type: application/json
{
"errors": [
{ "base": "domain cannot be unlocked" }
]
}
```
```
HTTP/1.1 404
Content-Type: application/json
{
"errors": [
{ "base": "domain does not exist" }
]
}
```

View file

@ -0,0 +1,117 @@
require 'test_helper'
require 'auth_token/auth_token_creator'
class RegistrantApiContactsTest < ApplicationIntegrationTest
def setup
super
@original_registry_time = Setting.days_to_keep_business_registry_cache
Setting.days_to_keep_business_registry_cache = 1
travel_to Time.zone.parse('2010-07-05')
@user = users(:registrant)
@auth_headers = { 'HTTP_AUTHORIZATION' => auth_token }
end
def teardown
super
Setting.days_to_keep_business_registry_cache = @original_registry_time
travel_back
end
def test_root_returns_domain_list
get '/api/v1/registrant/contacts', {}, @auth_headers
assert_equal(200, response.status)
json_body = JSON.parse(response.body, symbolize_names: true)
assert_equal(5, json_body.count)
array_of_contact_codes = json_body.map { |x| x[:code] }
assert(array_of_contact_codes.include?('william-001'))
assert(array_of_contact_codes.include?('jane-001'))
end
def test_root_accepts_limit_and_offset_parameters
get '/api/v1/registrant/contacts', { 'limit' => 1, 'offset' => 0 }, @auth_headers
response_json = JSON.parse(response.body, symbolize_names: true)
assert_equal(200, response.status)
assert_equal(1, response_json.count)
get '/api/v1/registrant/contacts', {}, @auth_headers
response_json = JSON.parse(response.body, symbolize_names: true)
assert_equal(5, response_json.count)
end
def test_get_contact_details_by_uuid
get '/api/v1/registrant/contacts/0aa54704-d6f7-4ca9-b8ca-2827d9a4e4eb', {}, @auth_headers
assert_equal(200, response.status)
contact = JSON.parse(response.body, symbolize_names: true)
assert_equal('william@inbox.test', contact[:email])
end
def test_root_returns_503_when_business_registry_is_not_available
raise_not_available = -> (a, b) { raise Soap::Arireg::NotAvailableError.new({}) }
BusinessRegistryCache.stub :fetch_by_ident_and_cc, raise_not_available do
get '/api/v1/registrant/contacts', {}, @auth_headers
assert_equal(503, response.status)
response_json = JSON.parse(response.body, symbolize_names: true)
assert_equal({ errors: [base: ['Business Registry not available']] }, response_json)
end
end
def test_get_contact_details_by_uuid_returns_404_for_non_existent_contact
get '/api/v1/registrant/contacts/nonexistent-uuid', {}, @auth_headers
assert_equal(404, response.status)
response_json = JSON.parse(response.body, symbolize_names: true)
assert_equal({ errors: [{ base: ['Contact not found'] }] }, response_json)
end
def test_root_does_not_accept_limit_higher_than_200
get '/api/v1/registrant/contacts', { 'limit' => 400, 'offset' => 0 }, @auth_headers
assert_equal(400, response.status)
response_json = JSON.parse(response.body, symbolize_names: true)
assert_equal({ errors: [{ limit: ['parameter is out of range'] }] }, response_json)
end
def test_root_does_not_accept_offset_lower_than_0
get '/api/v1/registrant/contacts', { 'limit' => 200, 'offset' => "-10" }, @auth_headers
assert_equal(400, response.status)
response_json = JSON.parse(response.body, symbolize_names: true)
assert_equal({ errors: [{ offset: ['parameter is out of range'] }] }, response_json)
end
def test_root_returns_401_without_authorization
get '/api/v1/registrant/contacts', {}, {}
assert_equal(401, response.status)
json_body = JSON.parse(response.body, symbolize_names: true)
assert_equal({ errors: [base: ['Not authorized']] }, json_body)
end
def test_details_returns_401_without_authorization
get '/api/v1/registrant/contacts/c0a191d5-3793-4f0b-8f85-491612d0293e', {}, {}
assert_equal(401, response.status)
json_body = JSON.parse(response.body, symbolize_names: true)
assert_equal({ errors: [base: ['Not authorized']] }, json_body)
end
def test_details_returns_404_for_non_existent_contact
get '/api/v1/registrant/contacts/some-random-uuid', {}, @auth_headers
assert_equal(404, response.status)
json_body = JSON.parse(response.body, symbolize_names: true)
assert_equal({ errors: [base: ['Contact not found']] }, json_body)
end
private
def auth_token
token_creator = AuthTokenCreator.create_with_defaults(@user)
hash = token_creator.token_in_hash
"Bearer #{hash[:access_token]}"
end
end