diff --git a/api.yml b/api.yml new file mode 100644 index 0000000..5d8e720 --- /dev/null +++ b/api.yml @@ -0,0 +1,402 @@ +openapi: '3.0.0' +info: + description: '' + version: 0.1.0-dev + title: Nomilo + + +components: + securitySchemes: + ApiToken: + type: http + scheme: bearer + bearerFormat: JWT + + parameters: + ZoneName: + name: zone + in: path + schema: + type: string + required: true + + schemas: + UserRequest: + type: object + required: + - username + - password + - email + properties: + username: + type: string + password: + type: string + email: + type: string + role: + type: string + enum: + - admin + - zoneadmin + + TokenRequest: + type: object + required: + - username + - password + properties: + username: + type: string + password: + type: string + + TokenResponse: + type: object + required: + - token + properties: + token: + type: string + + AddZoneMemberRequest: + type: object + required: + - id + properties: + id: + type: string + + CreateZoneRequest: + type: object + required: + - name + properties: + name: + type: string + + Zone: + type: object + required: + - name + properties: + name: + type: string + + ZoneList: + type: array + items: + $ref: '#/components/schemas/Zone' + + RecordBase: + type: object + required: + - Name + - Class + - TTL + - Type + properties: + Name: + type: string + Class: + type: string + enum: + - IN + - CH + - HS + - NONE + - ANY + TTL: + type: integer + Type: + type: string + + RecordTypeA: + type: object + allOf: + - $ref: '#/components/schemas/RecordBase' + - type: object + required: + - Address + properties: + Address: + type: string + + RecordTypeAAAA: + type: object + allOf: + - $ref: '#/components/schemas/RecordBase' + - type: object + required: + - Address + properties: + Address: + type: string + + RecordTypeCAA: + type: object + allOf: + - $ref: '#/components/schemas/RecordBase' + required: + - IssuerCritical + - Value + - PropertyTag + properties: + IssuerCritical: + type: boolean + Value: + type: string + PropertyTag: + type: string + + RecordTypeCNAME: + type: object + allOf: + - $ref: '#/components/schemas/RecordBase' + - type: object + required: + - Target + properties: + Target: + type: string + + RecordTypeMX: + type: object + allOf: + - $ref: '#/components/schemas/RecordBase' + - type: object + required: + - Preference + - MailExchanger + properties: + Preference: + type: integer + MailExchanger: + type: string + + RecordTypeNS: + type: object + allOf: + - $ref: '#/components/schemas/RecordBase' + - type: object + required: + - Target + properties: + Target: + type: string + + RecordTypePTR: + type: object + allOf: + - $ref: '#/components/schemas/RecordBase' + - type: object + required: + - Target + properties: + Target: + type: string + + RecordTypeSOA: + type: object + allOf: + - $ref: '#/components/schemas/RecordBase' + - type: object + required: + - MasterServerName + - MaintainerName + - Refresh + - Retry + - Expire + - Minimum + - Serial + properties: + MasterServerName: + type: string + MaintainerName: + type: string + Refresh: + type: integer + Retry: + type: integer + Expire: + type: integer + Minimum: + type: integer + Serial: + type: integer + + RecordTypeSRV: + type: object + allOf: + - $ref: '#/components/schemas/RecordBase' + - type: object + required: + - Server + - Port + - Priority + - Weight + properties: + Server: + type: string + Port: + type: integer + Priority: + type: integer + Weight: + type: integer + + RecordTypeSSHFP: + type: object + allOf: + - $ref: '#/components/schemas/RecordBase' + - type: object + required: + - Algorithm + - DigestType + - Fingerprint + properties: + Algorithm: + type: integer + DigestType: + type: integer + Fingerprint: + type: string + + RecordTypeTXT: + type: object + allOf: + - $ref: '#/components/schemas/RecordBase' + - type: object + required: + - Text + properties: + Text: + type: string + + Record: + type: object + oneOf: + - $ref: '#/components/schemas/RecordTypeA' + - $ref: '#/components/schemas/RecordTypeAAAA' + - $ref: '#/components/schemas/RecordTypeCAA' + - $ref: '#/components/schemas/RecordTypeCNAME' + - $ref: '#/components/schemas/RecordTypeMX' + - $ref: '#/components/schemas/RecordTypeNS' + - $ref: '#/components/schemas/RecordTypePTR' + - $ref: '#/components/schemas/RecordTypeSOA' + - $ref: '#/components/schemas/RecordTypeSRV' + - $ref: '#/components/schemas/RecordTypeSSHFP' + - $ref: '#/components/schemas/RecordTypeTXT' + discriminator: + propertyName: Type + mapping: + A: '#/components/schemas/RecordTypeA' + AAAA: '#/components/schemas/RecordTypeAAAA' + CAA: '#/components/schemas/RecordTypeCAA' + CNAME: '#/components/schemas/RecordTypeCNAME' + MX: '#/components/schemas/RecordTypeMX' + NS: '#/components/schemas/RecordTypeNS' + PTR: '#/components/schemas/RecordTypePTR' + SOA: '#/components/schemas/RecordTypeSOA' + SRV: '#/components/schemas/RecordTypeSRV' + SSHFP: '#/components/schemas/RecordTypeSSHFP' + TXT: '#/components/schemas/RecordTypeTXT' + + RecordList: + type: array + items: + $ref: '#/components/schemas/Record' + + +paths: + '/users': + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UserRequest' + responses: + '201': + description: '' + + '/users/me/token': + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TokenRequest' + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/TokenResponse' + '/zones': + get: + security: + - ApiToken: [] + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ZoneList' + post: + security: + - ApiToken: [] + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CreateZoneRequest' + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/Zone' + + '/zones/{zone}/members': + parameters: + - $ref: '#/components/parameters/ZoneName' + post: + security: + - ApiToken: [] + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AddZoneMemberRequest' + responses: + '201': + description: '' + + '/zones/{zone}/records': + parameters: + - $ref: '#/components/parameters/ZoneName' + get: + security: + - ApiToken: [] + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/RecordList' + post: + security: + - ApiToken: [] + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/RecordList' + responses: + '200': + description: '' diff --git a/e2e/zones.py b/e2e/zones.py new file mode 100644 index 0000000..0e0ec3a --- /dev/null +++ b/e2e/zones.py @@ -0,0 +1,99 @@ +from nomilo_client import ApiClient, Configuration +from nomilo_client.api.default_api import DefaultApi +from nomilo_client.models import ( + TokenRequest, + RecordTypeSOA, + RecordTypeAAAA, + RecordTypeCNAME, + RecordTypeNS, + RecordTypeTXT, + RecordList +) + +import logging +import string +import random + +import unittest +import warnings + + +logging.basicConfig(level=logging.DEBUG) + +HOST = 'http://localhost:8000/api/v1' +USER='toto' +PASSWORD='supersecure' + + +def build_api(host: str): + conf = Configuration(host=HOST) + api_client = ApiClient(configuration=conf) + return DefaultApi(api_client) + +def build_authenticated_api(host: str, token: TokenRequest): + auth_conf = Configuration(host=host, access_token=token.token) + api_client = ApiClient(configuration=auth_conf) + return DefaultApi(api_client) + +def random_string(length): + return ''.join(random.choice(string.ascii_lowercase) for x in range(length)) + +def random_name(zone): + return '%s.%s' % (random_string(16), zone) + + +class TestZones(unittest.TestCase): + @classmethod + def setUpClass(cls): + # Ignore warning about unclosed socket + warnings.filterwarnings(action="ignore", message="unclosed", category=ResourceWarning) + + api = build_api(HOST) + token = api.users_me_token_post(token_request=TokenRequest(username=USER,password=PASSWORD)) + cls.api = build_authenticated_api(HOST, token) + + def test_get_zones(self): + zones = self.api.zones_get() + zone_name = zones.value[0].name + self.assertEqual(zone_name, 'example.com.') + + def test_get_records(self): + records = self.api.zones_zone_records_get(zone='example.com.') + for record in records.value: + if type(record) is RecordTypeSOA: + with self.subTest(type='soa'): + self.assertEqual(record.name, 'example.com.') + + if type(record) is RecordTypeAAAA: + with self.subTest(type='ns'): + self.assertEqual(record.name, 'srv1.example.com.') + self.assertEqual(record.address, '2001:db8:cafe:bc68::2') + + if type(record) is RecordTypeCNAME: + with self.subTest(type='cname'): + self.assertEqual(record.name, 'www.example.com.') + self.assertEqual(record.target, 'srv1.example.com.') + + if type(record) is RecordTypeNS: + with self.subTest(type='ns'): + self.assertEqual(record.name, 'example.com.') + self.assertEqual(record.target, 'ns.example.com.') + + def test_create_records(self): + new_record = RecordTypeTXT( + _class='IN', + ttl=300, + name=random_name('example.com.'), + text=random_string(32), + type='TXT' + ) + + self.api.zones_zone_records_post(zone='example.com.', record_list=RecordList(value=[new_record])) + records = self.api.zones_zone_records_get(zone='example.com.') + found = False + for record in records.value: + if type(record) is RecordTypeTXT and record.name == new_record.name: + self.assertEqual(record.text, new_record.text, msg='New record does not have the expected value') + found = True + + self.assertTrue(found, msg='New record not found in zone records')