Skip to content

Commit

Permalink
Add Njalla provider (#497)
Browse files Browse the repository at this point in the history
* 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
chapatt and adferrand authored Aug 21, 2020
1 parent ef3e5db commit 59579df
Show file tree
Hide file tree
Showing 28 changed files with 5,625 additions and 0 deletions.
1 change: 1 addition & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ lexicon/providers/namecheap.py @pschmitt @rbelnap
lexicon/providers/namesilo.py @analogj
lexicon/providers/netcup.py @coldfix
lexicon/providers/nfsn.py @tersers
lexicon/providers/njalla.py @chapatt
lexicon/providers/nsone.py @init-js @trinopoty
lexicon/providers/onapp.py @alexzorin
lexicon/providers/online.py @kapouer
Expand Down
294 changes: 294 additions & 0 deletions README.md

Large diffs are not rendered by default.

159 changes: 159 additions & 0 deletions lexicon/providers/njalla.py
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()
26 changes: 26 additions & 0 deletions lexicon/tests/providers/test_njalla.py
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
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
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
Loading

0 comments on commit 59579df

Please sign in to comment.