-
-
Notifications
You must be signed in to change notification settings - Fork 306
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add Njalla provider implementation * Add tests and recordings for Njalla provider * Add contact for Njalla * Correct linting errors * Remove nonpertinent records from recordings * Add Njalla to list of supported providers * [Njalla] implement and use _request method for api calls * Update Njalla integration tests * Update Njalla test fixtures * Fix code style errors Co-authored-by: Adrien Ferrand <[email protected]>
- Loading branch information
Showing
28 changed files
with
5,625 additions
and
0 deletions.
There are no files selected for viewing
Validating CODEOWNERS rules …
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
"""Module provider for Njalla""" | ||
from __future__ import absolute_import | ||
import logging | ||
|
||
import requests | ||
from lexicon.providers.base import Provider as BaseProvider | ||
|
||
|
||
LOGGER = logging.getLogger(__name__) | ||
|
||
NAMESERVER_DOMAINS = [ | ||
'1-you.njalla.no', | ||
'2-can.njalla.in', | ||
'3-get.njalla.fo', | ||
] | ||
|
||
|
||
def provider_parser(subparser): | ||
"""Module provider for Njalla""" | ||
subparser.add_argument( | ||
"--auth-token", help="specify API token for authentication") | ||
|
||
|
||
class Provider(BaseProvider): | ||
"""Provider class for Njalla""" | ||
def __init__(self, config): | ||
super(Provider, self).__init__(config) | ||
self.domain_id = None | ||
self.api_endpoint = 'https://njal.la/api/1/' | ||
|
||
def _authenticate(self): | ||
params = { | ||
'domain': self.domain, | ||
} | ||
result = self._api_call('get-domain', params) | ||
|
||
if result['name'] != self.domain: | ||
raise Exception('Domain not found') | ||
|
||
self.domain_id = self.domain | ||
|
||
# Create record. If record already exists with the same content, do nothing' | ||
def _create_record(self, rtype, name, content): | ||
params = { | ||
'domain': self.domain, | ||
'type': rtype, | ||
'name': name, | ||
'content': content, | ||
'ttl': 10800, | ||
} | ||
if self._get_lexicon_option('ttl'): | ||
params['ttl'] = self._get_lexicon_option('ttl') | ||
result = self._api_call('add-record', params) | ||
|
||
LOGGER.debug('create_record: %s', result) | ||
return result | ||
|
||
# List all records. Return an empty list if no records found | ||
# type, name and content are used to filter records. | ||
# If possible filter during the query, otherwise filter after response is received. | ||
def _list_records(self, rtype=None, name=None, content=None): | ||
params = { | ||
'domain': self.domain, | ||
} | ||
result = self._api_call('list-records', params) | ||
|
||
records = result['records'] | ||
processed_records = [{ | ||
'id': record['id'], | ||
'type': record['type'], | ||
'name': self._full_name(record['name']), | ||
'ttl': record['ttl'], | ||
'content': record['content'], | ||
} for record in records] | ||
filtered_records = [record for record in processed_records if ( | ||
(rtype is None or record['type'] == rtype) | ||
and (name is None or record['name'] == self._full_name(name)) | ||
and (content is None or record['content'] == content))] | ||
|
||
LOGGER.debug('list_records: %s', filtered_records) | ||
return filtered_records | ||
|
||
# Create or update a record. | ||
def _update_record(self, identifier, rtype=None, name=None, content=None): | ||
if not identifier: | ||
identifier = self._get_record_identifier(rtype=rtype, name=name) | ||
|
||
params = { | ||
'id': identifier, | ||
'domain': self.domain, | ||
'content': content, | ||
} | ||
result = self._api_call('edit-record', params) | ||
|
||
LOGGER.debug('update_record: %s', result) | ||
return result | ||
|
||
# Delete an existing record. | ||
# If record does not exist, do nothing. | ||
def _delete_record(self, identifier=None, rtype=None, name=None, content=None): | ||
if not identifier: | ||
identifier = self._get_record_identifier(rtype=rtype, name=name, content=content) | ||
|
||
params = { | ||
'domain': self.domain, | ||
'id': identifier, | ||
} | ||
self._api_call('remove-record', params) | ||
|
||
LOGGER.debug('delete_record: %s', True) | ||
return True | ||
|
||
# Helpers | ||
def _api_call(self, method, params): | ||
if self._get_provider_option('auth_token') is None: | ||
raise Exception('Must provide API token') | ||
|
||
data = { | ||
'method': method, | ||
'params': params, | ||
} | ||
response = self._request('POST', | ||
"", | ||
data) | ||
|
||
if 'error' in response.keys(): | ||
error = response['error'] | ||
raise Exception('%d: %s' % (error['code'], error['message'])) | ||
|
||
return response['result'] | ||
|
||
def _get_record_identifier(self, rtype=None, name=None, content=None): | ||
records = self._list_records(rtype=rtype, name=name, content=content) | ||
if len(records) == 1: | ||
return records[0]['id'] | ||
|
||
raise Exception('Unambiguous record could not be found.') | ||
|
||
def _request(self, action="GET", url="/", data=None, query_params=None): | ||
if data is None: | ||
data = {} | ||
if query_params is None: | ||
query_params = {} | ||
token = self._get_provider_option('auth_token') | ||
headers = { | ||
'Authorization': 'Njalla ' + token, | ||
'Content-Type': 'application/json', | ||
'Accept': 'application/json', | ||
} | ||
response = requests.request( | ||
action, | ||
self.api_endpoint + url, | ||
headers=headers, | ||
params=query_params, | ||
json=data, | ||
) | ||
# if the request fails for any reason, throw an error. | ||
response.raise_for_status() | ||
return response.json() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
"""Integration tests for Njalla provider""" | ||
from unittest import TestCase | ||
|
||
import pytest | ||
from lexicon.tests.providers.integration_tests import IntegrationTestsV2 | ||
|
||
# Hook into testing framework by inheriting unittest.TestCase and reuse | ||
# the tests which *each and every* implementation of the interface must | ||
# pass, by inheritance from integration_tests.IntegrationTests | ||
|
||
|
||
class NjallaProviderTests(TestCase, IntegrationTestsV2): | ||
"""TestCase for Njalla""" | ||
provider_name = 'njalla' | ||
domain = 'example.com' | ||
|
||
def _filter_headers(self): | ||
return ['Authorization'] | ||
|
||
@pytest.mark.skip(reason="provider allows duplicate records") | ||
def test_provider_when_calling_create_record_with_duplicate_records_should_be_noop(self): | ||
return | ||
|
||
@pytest.mark.skip(reason="provider does not recognize record sets") | ||
def test_provider_when_calling_delete_record_with_record_set_name_remove_all(self): | ||
return |
64 changes: 64 additions & 0 deletions
64
tests/fixtures/cassettes/njalla/IntegrationTests/test_provider_authenticate.yaml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
interactions: | ||
- request: | ||
body: !!python/unicode '{"params": {"domain": "example.com"}, "method": "get-domain"}' | ||
headers: | ||
Accept: | ||
- application/json | ||
Accept-Encoding: | ||
- gzip, deflate | ||
Connection: | ||
- keep-alive | ||
Content-Length: | ||
- '59' | ||
Content-Type: | ||
- application/json | ||
User-Agent: | ||
- python-requests/2.24.0 | ||
method: POST | ||
uri: https://njal.la/api/1/ | ||
response: | ||
body: | ||
string: !!python/unicode '{"result": {"name": "example.com", "status": "active", | ||
"expiry": "2021-06-29T23:53:03Z", "mailforwarding": false, "max_nameservers": | ||
10, "dnssec_type": "dsData"}, "jsonrpc": "2.0"} | ||
|
||
' | ||
headers: | ||
connection: | ||
- keep-alive | ||
content-length: | ||
- '179' | ||
content-security-policy: | ||
- script-src 'self' 'unsafe-inline' | ||
content-type: | ||
- application/json; charset=utf-8 | ||
date: | ||
- Thu, 20 Aug 2020 22:42:46 GMT | ||
onion-location: | ||
- http://njalladnspotetti.onion/api/1/ | ||
referrer-policy: | ||
- same-origin | ||
server: | ||
- nginx | ||
set-cookie: | ||
- csrftoken=vIFZ4pKrVGvZZ5wrmYQKLkMfMyAaHSFVH5uCLGWuwnOFlccz19xwD53mkk6xx3vv; | ||
expires=Thu, 19-Aug-2021 22:42:46 GMT; Max-Age=31449600; Path=/; Secure | ||
- sessionid=wllsz5gwhg1tp0n7j55jyzrdkju45prg; expires=Thu, 03-Sep-2020 22:42:46 | ||
GMT; HttpOnly; Max-Age=1209600; Path=/; Secure | ||
strict-transport-security: | ||
- max-age=63072000; includeSubDomains | ||
transfer-encoding: | ||
- chunked | ||
vary: | ||
- Accept-Encoding | ||
- Cookie | ||
x-content-type-options: | ||
- nosniff | ||
x-frame-options: | ||
- SAMEORIGIN | ||
x-xss-protection: | ||
- 1; mode=block | ||
status: | ||
code: 200 | ||
message: OK | ||
version: 1 |
64 changes: 64 additions & 0 deletions
64
...njalla/IntegrationTests/test_provider_authenticate_with_unmanaged_domain_should_fail.yaml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
interactions: | ||
- request: | ||
body: !!python/unicode '{"params": {"domain": "thisisadomainidonotown.com"}, "method": | ||
"get-domain"}' | ||
headers: | ||
Accept: | ||
- application/json | ||
Accept-Encoding: | ||
- gzip, deflate | ||
Connection: | ||
- keep-alive | ||
Content-Length: | ||
- '76' | ||
Content-Type: | ||
- application/json | ||
User-Agent: | ||
- python-requests/2.24.0 | ||
method: POST | ||
uri: https://njal.la/api/1/ | ||
response: | ||
body: | ||
string: !!python/unicode '{"error": {"code": 403, "message": "permission denied"}, | ||
"jsonrpc": "2.0"} | ||
|
||
' | ||
headers: | ||
connection: | ||
- keep-alive | ||
content-length: | ||
- '75' | ||
content-security-policy: | ||
- script-src 'self' 'unsafe-inline' | ||
content-type: | ||
- application/json; charset=utf-8 | ||
date: | ||
- Thu, 20 Aug 2020 22:42:46 GMT | ||
onion-location: | ||
- http://njalladnspotetti.onion/api/1/ | ||
referrer-policy: | ||
- same-origin | ||
server: | ||
- nginx | ||
set-cookie: | ||
- csrftoken=VonTQpL5fdG4twffYy5py2vLTiv5cBLcQ7xZzlJXFfpxxxucBTAHJrjS6XQCcilY; | ||
expires=Thu, 19-Aug-2021 22:42:46 GMT; Max-Age=31449600; Path=/; Secure | ||
- sessionid=oyvqp5anhlkuviiu569i7oawepxk3kic; expires=Thu, 03-Sep-2020 22:42:46 | ||
GMT; HttpOnly; Max-Age=1209600; Path=/; Secure | ||
strict-transport-security: | ||
- max-age=63072000; includeSubDomains | ||
transfer-encoding: | ||
- chunked | ||
vary: | ||
- Accept-Encoding | ||
- Cookie | ||
x-content-type-options: | ||
- nosniff | ||
x-frame-options: | ||
- SAMEORIGIN | ||
x-xss-protection: | ||
- 1; mode=block | ||
status: | ||
code: 200 | ||
message: OK | ||
version: 1 |
Oops, something went wrong.