├── .coveragerc ├── .editorconfig ├── .gitignore ├── .hgignore ├── .noserc ├── .travis.yml ├── CHANGES.rst ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── dnsaas ├── __init__.py ├── admin.py ├── api │ ├── __init__.py │ ├── urls.py │ ├── v2 │ │ ├── __init__.py │ │ ├── serializers.py │ │ ├── tests.py │ │ ├── urls.py │ │ └── views.py │ └── views.py ├── autocompletes.py ├── migrations │ └── __init__.py ├── settings.py ├── settings_test.py ├── urls.py ├── utils.py └── wsgi.py ├── doc ├── Makefile ├── api_tutorial │ ├── index.rst │ └── introduction.rst ├── conf.py ├── index.rst ├── installation │ ├── as_django_app.rst │ ├── as_microservice.rst │ ├── common_configuration.rst │ └── index.rst ├── permissions │ └── index.rst └── templating │ ├── auto_ptr.rst │ ├── domain_templates.rst │ └── index.rst ├── gulpfile.js ├── manage.py ├── package.json ├── powerdns ├── __init__.py ├── admin.py ├── apps.py ├── fixtures │ └── test.yaml ├── forms.py ├── locale │ ├── en │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ └── pl │ │ └── LC_MESSAGES │ │ ├── django.mo │ │ └── django.po ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20150514_0638.py │ ├── 0003_auto_20150527_0808.py │ ├── 0004_auto_20150528_0540.py │ ├── 0005_auto_20150609_0754.py │ ├── 0006_auto_20150612_0756.py │ ├── 0007_auto_20150710_0507.py │ ├── 0008_auto_20150821_0630.py │ ├── 0009_auto_20150915_0342.py │ ├── 0010_auto_20150921_0613.py │ ├── 0011_auto_20151103_0546.py │ ├── 0012_auto_20151112_0504.py │ ├── 0013_auto_20151124_0301.py │ ├── 0014_auto_20151124_0505.py │ ├── 0015_auto_20151214_0632.py │ ├── 0016_auto_20151217_0817.py │ ├── 0017_auto_20160104_0642.py │ ├── 0018_auto_20160105_0824.py │ ├── 0019_auto_20160111_0842.py │ ├── 0020_remove_recordrequest_target_ordername.py │ ├── 0021_tsigkey.py │ ├── 0022_auto_20160531_0843.py │ ├── 0023_domaintemplate_is_public_domain.py │ ├── 0024_auto_20160627_0658.py │ ├── 0025_auto_20160822_0137.py │ ├── 0026_auto_20160829_0337.py │ ├── 0027_auto_20160915_0349.py │ ├── 0028_auto_20160901_0235.py │ ├── 0029_auto_20160922_0757.py │ ├── 0030_auto_20161026_0944.py │ ├── 0031_auto_20161102_0640.py │ ├── 0032_auto_20161102_2006.py │ ├── 0033_auto_20161114_1442.py │ ├── 0034_auto_20161115_1452.py │ ├── 0035_auto_20161228_0058.py │ ├── 0036_auto_20171128_0119.py │ └── __init__.py ├── models │ ├── __init__.py │ ├── ownership.py │ ├── powerdns.py │ ├── requests.py │ ├── templates.py │ └── tsigkeys.py ├── routers.py ├── tests │ ├── __init__.py │ ├── test_auto_ptr.py │ ├── test_events.py │ ├── test_models.py │ ├── test_ownership.py │ ├── test_permissions.py │ ├── test_record_validators.py │ ├── test_request_uniqueness_constraints.py │ ├── test_requests.py │ ├── test_templates.py │ ├── test_uniqueness_constraints.py │ ├── test_utils.py │ └── utils.py ├── utils.py └── views.py ├── setup.py ├── tsconfig.json ├── typings.json ├── ui ├── __init__.py ├── static │ ├── app │ │ ├── app.component.html │ │ ├── app.component.ts │ │ ├── app.css │ │ ├── auth │ │ │ ├── auth.service.ts │ │ │ ├── index.ts │ │ │ ├── login.component.html │ │ │ ├── login.component.ts │ │ │ ├── logout.component.ts │ │ │ └── user.ts │ │ ├── autocomplete │ │ │ ├── autocomplete.component.html │ │ │ ├── autocomplete.component.ts │ │ │ └── autocomplete.service.ts │ │ ├── config.service.ts │ │ ├── directives │ │ │ └── highlight.directive.ts │ │ ├── domain │ │ │ ├── domain.component.html │ │ │ ├── domain.component.ts │ │ │ ├── domain.service.ts │ │ │ ├── domain.ts │ │ │ └── index.ts │ │ ├── exception-handler.ts │ │ ├── flash │ │ │ ├── flash.component.ts │ │ │ └── flash.service.ts │ │ ├── http-client.ts │ │ ├── index.ts │ │ ├── local-storage.spec.ts │ │ ├── local-storage.ts │ │ ├── main.ts │ │ ├── pagination │ │ │ └── pagination.component.ts │ │ ├── record-request │ │ │ ├── index.ts │ │ │ ├── record-request-detail.component.html │ │ │ ├── record-request-detail.component.ts │ │ │ ├── record-request.component.html │ │ │ ├── record-request.component.ts │ │ │ ├── record-request.service.ts │ │ │ └── record-request.ts │ │ ├── record │ │ │ ├── index.ts │ │ │ ├── record-detail.component.html │ │ │ ├── record-detail.component.ts │ │ │ ├── record.component.html │ │ │ ├── record.component.ts │ │ │ ├── record.service.ts │ │ │ └── record.ts │ │ ├── search.component.ts │ │ ├── service │ │ │ ├── service.service.ts │ │ │ └── service.ts │ │ └── tooltip.directive.ts │ ├── pylabs.png │ ├── systemjs.config.js │ └── unit-tests.html ├── templates │ └── ui │ │ └── index.html └── views.py └── version.json /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = */migrations/*,*/south_migrations/* 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | # Unix-style newlines with a newline ending every file 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | charset = utf-8 13 | indent_style = space 14 | indent_size = 2 15 | end_of_line = lf 16 | 17 | [*.md] 18 | max_line_length = 0 19 | 20 | [*.{html,yml}] 21 | indent_style = space 22 | indent_size = 2 23 | 24 | [*.{css,js,json,py}] 25 | indent_style = space 26 | indent_size = 4 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | 7 | # cache files 8 | __pycache__ 9 | 10 | # dependencies 11 | /node_modules 12 | /bower_components 13 | 14 | # IDEs and editors 15 | /.idea 16 | 17 | # misc 18 | /.sass-cache 19 | /connect.lock 20 | /coverage/* 21 | /libpeerconnection.log 22 | npm-debug.log 23 | testem.log 24 | /typings 25 | 26 | # e2e 27 | /e2e/*.js 28 | /e2e/*.map 29 | 30 | #System Files 31 | .DS_Store 32 | ui/static/dist/ 33 | ui/static/lib/ 34 | -------------------------------------------------------------------------------- /.hgignore: -------------------------------------------------------------------------------- 1 | syntax: glob 2 | *.pyc 3 | *~ 4 | dist/* 5 | build/* 6 | example/settings_local.py 7 | django_powerdns_dnssec.egg-info/* 8 | .DS_Store 9 | .tox 10 | -------------------------------------------------------------------------------- /.noserc: -------------------------------------------------------------------------------- 1 | [nosetests] 2 | verbosity=3 3 | with-coverage=1 4 | cover-package=dnsaas,powerdns 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # TODO: 2 | # * run integration tests 3 | # * coverage 4 | # * multi-db (run tests on other versions of mysql (5.5, 5.6) and postgres) 5 | 6 | language: python 7 | 8 | env: 9 | global: 10 | - DNSAAS_DB_USER=root 11 | - DNSAAS_DB_PASSWORD="" 12 | - DJANGO_SETTINGS_MODULE=dnsaas.settings_test 13 | matrix: 14 | - DJANGO_VERSION=1.8.17 15 | - DJANGO_VERSION=1.9.12 16 | - DJANGO_VERSION=1.10.5 17 | matrix: 18 | allow_failures: 19 | - env: DJANGO_VERSION=1.10.5 20 | 21 | python: 22 | - "3.4" 23 | 24 | before_install: 25 | - pip install -U flake8==3.0.4 pip 26 | - make flake 27 | 28 | install: 29 | - make install-test 30 | - pip install django==$DJANGO_VERSION 31 | 32 | script: 33 | - make test 34 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | Change Log 2 | ---------- 3 | 4 | 2.0 5 | ~~~~~ 6 | 7 | * Templates for domains and records 8 | * Automated PTR 9 | * Domain and record ownership 10 | * REST API via django_rest_framework 11 | * Lot of validation improvements 12 | * Improved unittest code coverage 13 | * Added integration tests via docker 14 | * Created docker configuration for testing purposes 15 | 16 | 0.9.3 17 | ~~~~~ 18 | 19 | * Fixed issue #3: HTTP 500 in record admin form if no type given 20 | 21 | 0.9.2 22 | ~~~~~ 23 | 24 | * Fixed issue #2: numeric sorting of IP addresses in admin 25 | 26 | 0.9.1 27 | ~~~~~ 28 | 29 | * Domain foreign keys support auto completion and have "Edit separately" links 30 | 31 | * Field choices use radio selects whenever that makes sense (fever clicks 32 | necessary) 33 | 34 | * Forward/reverse domain filter (requires Django 1.4+) 35 | 36 | * The ``0002`` database migration in 0.9.0 was incomplete, this is now fixed 37 | 38 | 0.9.0 39 | ~~~~~ 40 | 41 | * DNSSEC tables supported. 42 | 43 | * Support for multiple databases. 44 | 45 | * Updated the project to require at least Django 1.3. 46 | 47 | * UI translations supported (currently Polish translation added). 48 | 49 | * South migrations. 50 | 51 | * Source code compliant with PEP8. 52 | 53 | * Minor fixes. 54 | 55 | 0.2.0 56 | ~~~~~ 57 | 58 | * First release with basic PowerDNS support. 59 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Peter Nixon and individual contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 18 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 19 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 20 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 21 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst LICENSE version.json 2 | recursive-include powerdns/locale *.* 3 | recursive-include powerdns/templates *.* 4 | recursive-include ui/static *.* 5 | recursive-include ui/templates *.* 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | flake: 2 | flake8 --exclude migrations dnsaas/ powerdns/ 3 | 4 | install: 5 | pip install -e . .[dnsaas] 6 | 7 | install-test: install 8 | pip install .[tests] 9 | 10 | test: 11 | python manage.py test 12 | 13 | coveralls: 14 | coverage run $(shell which python) manage.py test 15 | coverage report 16 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-powerdns-dnssec 2 | =================================== 3 | 4 | Welcome to the PowerDNS app for Django. 5 | 6 | This application allows easy administration of PowerDNS records stored in an 7 | SQL database by leveraging the standard Django Admin app. You may also use the 8 | Django PowerDNS application as part of a larger project to programatically 9 | modify your DNS records. 10 | 11 | **Note:** This is an updated and enhanced fork of `django-powerdns 12 | `_ which looks abandoned as of 13 | November 2012. 14 | 15 | 16 | Project status 17 | -------------- 18 | This project is deprecated and not maintained anymore. 19 | 20 | 21 | Authors 22 | ------- 23 | 24 | Application written by `Peter Nixon `_ and 25 | `Łukasz Langa `_. NSEC3 code based on George Notaras' 26 | work with `django-powerdns-manager 27 | `_. 28 | The application is now developed by `Allegro pyLabs ` 29 | 30 | |ReadTheDocs|_ 31 | 32 | .. image:: https://travis-ci.org/allegro/django-powerdns-dnssec.svg?branch=develop 33 | :target: https://travis-ci.org/allegro/django-powerdns-dnssec 34 | .. |ReadTheDocs| image:: https://readthedocs.org/projects/django-powerdns-dnssec/badge/?version=latest 35 | .. _ReadTheDocs: http://django-powerdns-dnssec.readthedocs.org/en/latest/?badge=latest 36 | -------------------------------------------------------------------------------- /dnsaas/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allegro/django-powerdns-dnssec/333bdc668b6cda1a2ff240efd814a1896a1a8e07/dnsaas/__init__.py -------------------------------------------------------------------------------- /dnsaas/admin.py: -------------------------------------------------------------------------------- 1 | # Backwards-compatibility 2 | from powerdns.admin import * # noqa 3 | -------------------------------------------------------------------------------- /dnsaas/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allegro/django-powerdns-dnssec/333bdc668b6cda1a2ff240efd814a1896a1a8e07/dnsaas/api/__init__.py -------------------------------------------------------------------------------- /dnsaas/api/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import include, url 2 | 3 | from dnsaas.api import views 4 | from powerdns.utils import patterns 5 | 6 | 7 | urlpatterns = patterns( 8 | '', 9 | url(r'^v2/', include('dnsaas.api.v2.urls', namespace='v2')), 10 | url(r'', views.api_not_available, name='api_not_available'), 11 | ) 12 | -------------------------------------------------------------------------------- /dnsaas/api/v2/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allegro/django-powerdns-dnssec/333bdc668b6cda1a2ff240efd814a1896a1a8e07/dnsaas/api/v2/__init__.py -------------------------------------------------------------------------------- /dnsaas/api/v2/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | from rest_framework.routers import DefaultRouter 4 | 5 | from powerdns.utils import patterns 6 | 7 | from .views import ( 8 | CryptoKeyViewSet, 9 | DomainMetadataViewSet, 10 | DomainTemplateViewSet, 11 | DomainViewSet, 12 | IPRecordView, 13 | RecordRequestsViewSet, 14 | RecordTemplateViewSet, 15 | RecordViewSet, 16 | ServiceViewSet, 17 | SuperMasterViewSet, 18 | TsigKeysViewSet, 19 | ) 20 | 21 | 22 | router = DefaultRouter() 23 | router.register(r'crypto-keys', CryptoKeyViewSet) 24 | router.register(r'domain-templates', DomainTemplateViewSet) 25 | router.register(r'domains', DomainViewSet) 26 | router.register(r'domains-metadata', DomainMetadataViewSet) 27 | router.register(r'record-requests', RecordRequestsViewSet) 28 | router.register(r'record-templates', RecordTemplateViewSet) 29 | router.register(r'records', RecordViewSet) 30 | router.register(r'service', ServiceViewSet) 31 | router.register(r'super-masters', SuperMasterViewSet) 32 | router.register(r'tsigkeys', TsigKeysViewSet) 33 | urlpatterns = router.urls 34 | urlpatterns += patterns( 35 | '', 36 | url(r'^ip_record/', IPRecordView.as_view(), name='ip-record'), 37 | ) 38 | -------------------------------------------------------------------------------- /dnsaas/api/views.py: -------------------------------------------------------------------------------- 1 | from django.core.urlresolvers import reverse 2 | from django.http import HttpResponse 3 | 4 | 5 | def api_not_available(request): 6 | html = """ 7 | 8 | API v1 was removed. Please use new API v2, available at: 9 | {url} 10 | 11 | """.format(url=reverse('api:v2:api-root')) 12 | return HttpResponse(html) 13 | -------------------------------------------------------------------------------- /dnsaas/autocompletes.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | 3 | from dal import autocomplete 4 | 5 | 6 | class UserAutocomplete(autocomplete.Select2QuerySetView): 7 | queryset = get_user_model().objects.all() 8 | 9 | def get_queryset(self): 10 | qs = self.queryset 11 | if self.q: 12 | qs = qs.filter(username__icontains=self.q) 13 | return qs 14 | -------------------------------------------------------------------------------- /dnsaas/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allegro/django-powerdns-dnssec/333bdc668b6cda1a2ff240efd814a1896a1a8e07/dnsaas/migrations/__init__.py -------------------------------------------------------------------------------- /dnsaas/settings_test.py: -------------------------------------------------------------------------------- 1 | from dnsaas.settings import * # noqa 2 | 3 | USE_TZ = True 4 | -------------------------------------------------------------------------------- /dnsaas/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import include, url 2 | from django.contrib import admin 3 | from django.contrib.admin.views.decorators import staff_member_required 4 | from django.conf import settings 5 | 6 | from dal import autocomplete 7 | from dnsaas.autocompletes import UserAutocomplete 8 | from dnsaas.utils import VERSION 9 | from powerdns.models import ( 10 | Domain, 11 | Record, 12 | Service, 13 | ) 14 | from powerdns.utils import patterns 15 | from powerdns.views import obtain_auth_token 16 | from ui.views import start_page 17 | 18 | 19 | title = settings.SITE_TITLE 20 | title_v = ' '.join([title, VERSION]) 21 | 22 | admin.site.site_title = title 23 | admin.site.site_header = title_v 24 | admin.autodiscover() 25 | 26 | 27 | autocomplete_urlpatterns = [ 28 | url( 29 | 'users/$', 30 | staff_member_required( 31 | UserAutocomplete.as_view() 32 | ), 33 | name='users', 34 | ), 35 | url( 36 | 'domains/$', 37 | staff_member_required( 38 | autocomplete.Select2QuerySetView.as_view(model=Domain) 39 | ), 40 | name='domains', 41 | ), 42 | url( 43 | 'records/$', 44 | staff_member_required( 45 | autocomplete.Select2QuerySetView.as_view(model=Record) 46 | ), 47 | name='records', 48 | ), 49 | url( 50 | 'services/$', 51 | staff_member_required( 52 | autocomplete.Select2QuerySetView.as_view(model=Service) 53 | ), 54 | name='services', 55 | ), 56 | ] 57 | 58 | 59 | urlpatterns = patterns( 60 | '', 61 | url(r'^admin/', include(admin.site.urls)), 62 | url( 63 | r'^autocomplete/', 64 | include(autocomplete_urlpatterns, namespace='autocomplete') 65 | ), 66 | url(r'^api/', include('dnsaas.api.urls', namespace='api')), 67 | url(r'^api-token-auth/', obtain_auth_token, name='get-api-token'), 68 | url(r'^api-docs/', include('rest_framework_swagger.urls')), 69 | url(r'^$', start_page, name='home'), 70 | ) 71 | -------------------------------------------------------------------------------- /dnsaas/utils.py: -------------------------------------------------------------------------------- 1 | from pkg_resources import working_set, Requirement 2 | 3 | 4 | try: 5 | req = Requirement.parse('django-powerdns-dnssec') 6 | VERSION = working_set.find(req).version 7 | except AttributeError: 8 | import json 9 | with open('version.json') as f: 10 | VERSION = '.'.join(str(part) for part in json.load(f)) 11 | -------------------------------------------------------------------------------- /dnsaas/wsgi.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.core.wsgi import get_wsgi_application 4 | 5 | 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dnsaas.settings") 7 | application = get_wsgi_application() 8 | -------------------------------------------------------------------------------- /doc/api_tutorial/index.rst: -------------------------------------------------------------------------------- 1 | Api tutorial 2 | =================== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | introduction 8 | -------------------------------------------------------------------------------- /doc/api_tutorial/introduction.rst: -------------------------------------------------------------------------------- 1 | Introduction 2 | ====================== 3 | 4 | To start the server to test API, consult 5 | :doc:`this instructions<../installation/testing>` 6 | 7 | 8 | Browsing API documentation 9 | ================================= 10 | 11 | `swagger_` is used to generate API documentation. Navigate to 12 | http://127.0.0.1:8080/api-docs to browse the documentation. 13 | 14 | Sending API requests 15 | ============================ 16 | 17 | Please note, that ``django-powerdns-dnssec`` uses `rest_framework`_ to handle 18 | the API access. Therefore all the perks offered by it are available. Including 19 | the web browsable API. If you navigate to http://127.0.0.1:8080/api you should 20 | see a nice interface where you can play around with the API without any 21 | additional tools. 22 | .. _`rest_framework` http://www.django-rest-framework.org/ 23 | 24 | Of course you can also send API requests with your favourite library or browser 25 | plugin. 26 | 27 | 28 | 29 | Filtering 30 | ========= 31 | 32 | Endpoint `/api/records/` 33 | ------------------------ 34 | 35 | Two extra options are present: 36 | 37 | `ip`: gets `records` related to filtering `IP` 38 | 39 | example urls: 40 | - http://localhost:8080/api/records/?ip=192.168.0.1 41 | returns all records related to "192.168.0.1" IP 42 | - http://localhost:8080/api/records/?ip=192.168.0.1&ip=192.168.0.2 43 | returns all records related to "192.168.0.1" or "192.168.0.2" IPs 44 | 45 | 46 | `type`: gets `records` related to filtering `types` ('A', 'CNAME', etc.) 47 | 48 | example urls: 49 | - http://localhost:8080/api/records/?type=NS 50 | returns all records with type=NS 51 | - http://localhost:8080/api/records/?type=NS&type=A 52 | returns all records with type=NS OR type=A 53 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | .. django-powerdns-dnssec documentation master file, created by 2 | sphinx-quickstart on Wed Jun 17 14:13:00 2015. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to django-powerdns-dnssec's documentation! 7 | ================================================== 8 | 9 | django-powerdns-dnssec is a solution for managing your PowerDNS SQL database 10 | via Python. You can use it basically in two ways: 11 | 12 | * As a standalone microservice 13 | * As a django app 14 | 15 | 16 | Contents: 17 | 18 | .. toctree:: 19 | :maxdepth: 2 20 | 21 | installation/index 22 | api_tutorial/index 23 | permissions/index 24 | templating/index 25 | 26 | Indices and tables 27 | ================== 28 | 29 | * :ref:`genindex` 30 | * :ref:`modindex` 31 | * :ref:`search` 32 | 33 | -------------------------------------------------------------------------------- /doc/installation/as_django_app.rst: -------------------------------------------------------------------------------- 1 | Installation as django application 2 | ==================================== 3 | 4 | Use pip to install ``django-powerdns-dnssec`` and all its dependencies from PyPI: 5 | 6 | .. code-block:: bash 7 | 8 | $ pip install django-powerdns-dnssec 9 | 10 | then add ``powerdns`` and ``django_extensions`` to ``INSTALLED_APPS`` in your project's ``settings.py``: 11 | 12 | ..code-block:: python 13 | 14 | INSTALLED_APPS = [ 15 | # ... 16 | 'powerdns', 17 | # ... 18 | ] 19 | -------------------------------------------------------------------------------- /doc/installation/as_microservice.rst: -------------------------------------------------------------------------------- 1 | Installation as a microservice 2 | ==================================== 3 | 4 | Use pip to install ``django-powerdns-dnssec[dnsaas]`` 5 | 6 | ..code-block:: bash 7 | 8 | $ pip install django-powerdns-dnssec[dnsaas] 9 | 10 | this will install all dependencies for the DNS-as-a-service microservice. 11 | 12 | You can use the attached ``dnsaas/settings.py`` as a base for your configuration or use a ``settings_local.py``. 13 | -------------------------------------------------------------------------------- /doc/installation/common_configuration.rst: -------------------------------------------------------------------------------- 1 | Common configuration options 2 | =================================== 3 | 4 | Customizing resource record types 5 | -------------------------------------- 6 | 7 | If you're not using DNSSEC or the other less common record types, you can 8 | simplify the user interface even more by specifying a sequence of types the app 9 | should use. Simply put this in your ``settings.py``:: 10 | 11 | POWERDNS_RECORD_TYPES = ( 12 | 'A', 'AAAA', 'CNAME', 'HINFO', 'MX', 'NAPTR', 'NS', 13 | 'PTR', 'SOA', 'SRV', 'TXT', 14 | ) 15 | 16 | Consult PowerDNS documentation for a `list of supported resource record types 17 | `_. 18 | 19 | Notifications 20 | ------------------------ 21 | 22 | This app notifies owners of domains and records that were created for them. To 23 | have them working you need to: 24 | 25 | * enable them by setting ``ENABLE_OWNER_NOTIFICATIONS`` 26 | 27 | * configure ``FROM_EMAIL`` (the e-mail that would be a sender of the 28 | notifications) 29 | 30 | * configure ``OWNER_NOTIFICATIONS`` as following:: 31 | 32 | OWNER_NOTIFICATIONS = { 33 | 'Domain': (subject, content), 34 | 'Record': (subject, content), 35 | } 36 | 37 | * set up the `mailing backend 38 | `_ 39 | 40 | 41 | In the subject and content strings you can place the following placeholders: 42 | 43 | * ``object`` - the string representation of the domain or record created 44 | * ``owner-email`` 45 | * ``owner-name`` 46 | * ``creator-email`` 47 | * ``creator-name`` 48 | 49 | 50 | Using a separate database for PowerDNS 51 | -------------------------------------- 52 | 53 | If your Django application is using a different database than the one used by 54 | PowerDNS, provide the configuration for the DNS database in ``settings.py`` as 55 | a separate entry in ``DATABASES``, for example:: 56 | 57 | DATABASES = { 58 | 'default': { 59 | 'ENGINE': 'django.db.backends.mysql', 60 | 'NAME': 'project_db', 61 | 'USER': 'user', 62 | 'PASSWORD': 'secret', 63 | 'HOST': '127.0.0.1', 64 | 'PORT': '3306', 65 | 'OPTIONS': { 66 | "init_command": "SET storage_engine=INNODB", 67 | }, 68 | }, 69 | 'powerdns': { 70 | 'ENGINE': 'django.db.backends.mysql', 71 | 'NAME': 'powerdns', 72 | 'USER': 'pdns', 73 | 'PASSWORD': 'pdns', 74 | 'HOST': '127.0.0.1', 75 | 'PORT': '3306', 76 | 'OPTIONS': { 77 | "init_command": "SET storage_engine=INNODB", 78 | }, 79 | }, 80 | } 81 | 82 | For Django to automatically route ``powerdns`` requests to the right database, 83 | add this setting to ``settings.py``:: 84 | 85 | DATABASE_ROUTERS = ['powerdns.routers.PowerDNSRouter'] 86 | 87 | You have to sync and migrate the ``default`` and the ``powerdns`` databases 88 | separately. First the default database:: 89 | 90 | $ python manage.py syncdb 91 | $ python manage.py migrate 92 | 93 | Then the ``powerdns`` database:: 94 | 95 | $ python manage.py syncdb --database=powerdns 96 | $ python manage.py migrate --database==powerdns powerdns 97 | 98 | Note that the ``powerdns`` database will maintain its own separate 99 | migration history table. This is especially helpful if your connecting several 100 | Django projects to a single PowerDNS database. 101 | 102 | -------------------------------------------------------------------------------- /doc/installation/index.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ==================== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | testing 8 | as_microservice 9 | as_django_app 10 | common_configuration 11 | -------------------------------------------------------------------------------- /doc/permissions/index.rst: -------------------------------------------------------------------------------- 1 | =============================== 2 | Notes on permission system 3 | =============================== 4 | 5 | In order to allow wide range of users to access your company's DNSaaS the 6 | ``django-powerdns-dnssec`` offers a permission system to prohibit unwanted 7 | changes. If you do not need any restrictions, simply register all your users 8 | as superusers in django. 9 | 10 | The permissions are controlled primarily by the 'owner' field of a Domain or 11 | Record, which is automatically set to the user that created the object. By 12 | default this is the only user that can modify it. Adding records to domains and 13 | subdomaining domains is also restricted. 14 | 15 | TODO: 16 | 17 | * Authorisations 18 | * Requests 19 | -------------------------------------------------------------------------------- /doc/templating/auto_ptr.rst: -------------------------------------------------------------------------------- 1 | Auto PTR 2 | ================ 3 | 4 | Normally when you create an A record, you also want to create a PTR record. 5 | 6 | There are three options for auto_ptr on a record: 7 | 8 | * **ALWAYS** - create a PTR for every A record 9 | * **NEVER** - don't create PTR's automatically 10 | * **ONLY-IF-DOMAIN-EXISTS** - create PTR if the reverse domain 11 | is already present 12 | 13 | If there is no domain for the PTR record and you selected **ALWAYS**, 14 | a new one will be created: 15 | 16 | * If the domain where you created the A record has ``reverse_template`` 17 | specified, this template will be used to created the domain for PTR. 18 | * Otherwise the template with name specified in 19 | DNSAAS_DEFAULT_REVERSE_DOMAIN_TEMPLATE setting will be used. 20 | -------------------------------------------------------------------------------- /doc/templating/domain_templates.rst: -------------------------------------------------------------------------------- 1 | Domain templates 2 | ======================== 3 | 4 | If you have several "boilerplate" DNS records that you need to configure for 5 | every new domain (most typically this would be SOA and NS records) you can use 6 | domain templates to reduce the amount of work needed. 7 | 8 | 1. Create a domain template object via admin selecting a name that would fit 9 | the type of domain that would need the configuration. 10 | 2. Create record templates bound to the above domain template. You can create 11 | record templates in a similar way to records. Anywhere in ``name`` and 12 | ``content`` fields you can use a placeholder ``{domain-name}`` to insert the 13 | name of an actual domain. 14 | 3. When creating a domain - select the appropriate template. The predefined 15 | records will be created automatically. 16 | -------------------------------------------------------------------------------- /doc/templating/index.rst: -------------------------------------------------------------------------------- 1 | Templating 2 | ======================= 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | domain_templates 8 | auto_ptr 9 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | const del = require('del'); 3 | const typescript = require('gulp-typescript'); 4 | const tscConfig = require('./tsconfig.json'); 5 | const sourcemaps = require('gulp-sourcemaps'); 6 | 7 | const appPath = 'ui/static/' 8 | 9 | 10 | gulp.task('clean', function () { 11 | return del(appPath + 'dist/**/*'); 12 | }); 13 | 14 | 15 | gulp.task('compile', ['clean'], function () { 16 | return gulp 17 | .src(['typings/browser.d.ts', appPath + 'app/**/*.ts']) 18 | .pipe(sourcemaps.init()) 19 | .pipe(typescript(tscConfig.compilerOptions)) 20 | .pipe(sourcemaps.write('.')) 21 | .pipe(gulp.dest(appPath + 'dist')); 22 | }); 23 | 24 | 25 | gulp.task('tslint', function() { 26 | var tslint = require('gulp-tslint'); 27 | return gulp.src(appPath + 'app/**/*.ts') 28 | .pipe(tslint()) 29 | .pipe(tslint.report('verbose')); 30 | }); 31 | 32 | 33 | gulp.task("libs", function () { 34 | return gulp.src([ 35 | 'es6-shim/es6-shim.min.js', 36 | 'systemjs/dist/system-polyfills.js', 37 | 'systemjs/dist/system.js', 38 | 'reflect-metadata/Reflect.js', 39 | 'raven-js/dist/raven.min.js', 40 | 'rxjs/util/isFunction.js', 41 | 'rxjs/Observer.js', 42 | 'rxjs/util/isArray.js', 43 | 'rxjs/util/isObject.js', 44 | 'rxjs/util/tryCatch.js', 45 | 'rxjs/util/errorObject.js', 46 | 'rxjs/util/UnsubscriptionError.js', 47 | 'rxjs/Observable.js', 48 | 'rxjs/Subject.js', 49 | 'rxjs/observable/PromiseObservable.js', 50 | 'rxjs/operator/toPromise.js', 51 | 'rxjs/add/observable/throw.js', 52 | 'rxjs/observable/throw.js', 53 | 'rxjs/add/operator/catch.js', 54 | 'rxjs/add/operator/map.js', 55 | 'rxjs/Subscriber.js', 56 | 'rxjs/Subscription.js', 57 | 'rxjs/SubjectSubscription.js', 58 | 'rxjs/symbol/rxSubscriber.js', 59 | 'rxjs/util/ObjectUnsubscribedError.js', 60 | 'rxjs/util/throwError.js', 61 | 'rxjs/symbol/observable.js', 62 | 'rxjs/util/root.js', 63 | 'rxjs/observable/ErrorObservable.js', 64 | 'rxjs/util/toSubscriber.js', 65 | 'rxjs/operator/catch.js', 66 | 'rxjs/operator/map.js', 67 | 'zone.js/dist/zone.min.js', 68 | '@angular/common/common.umd.js', 69 | '@angular/compiler/compiler.umd.js', 70 | '@angular/core/core.umd.js', 71 | '@angular/http/http.umd.js', 72 | '@angular/platform-browser/platform-browser.umd.js', 73 | '@angular/platform-browser-dynamic/platform-browser-dynamic.umd.js', 74 | '@angular/router-deprecated/router-deprecated.umd.js', 75 | '@angular/upgrade/upgrade.umd.js', 76 | 'bootstrap/dist/**', 77 | 'jquery/dist/**' 78 | ], {cwd: "node_modules/**"}) /* Glob required here. */ 79 | .pipe(gulp.dest(appPath + "lib/")); 80 | }); 81 | 82 | 83 | gulp.task("dev_libs", function () { 84 | return gulp.src([ 85 | 'jasmine-core/lib/jasmine-core/jasmine.css', 86 | 'jasmine-core/lib/jasmine-core/jasmine.js', 87 | 'jasmine-core/lib/jasmine-core/jasmine-html.js', 88 | 'jasmine-core/lib/jasmine-core/boot.js' 89 | ], {cwd: "node_modules/**"}) /* Glob required here. */ 90 | .pipe(gulp.dest(appPath + "lib/")); 91 | }); 92 | 93 | 94 | gulp.task('default', function() { 95 | gulp.start('compile', 'libs'); 96 | }); 97 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import sys 5 | 6 | if __name__ == "__main__": 7 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dnsaas.settings") 8 | 9 | from django.core.management import execute_from_command_line 10 | 11 | execute_from_command_line(sys.argv) 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dnsaas", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "tsc": "tsc", 6 | "tsc:w": "tsc -w", 7 | "test": "lite-server --open=ui/static/unit-tests.html", 8 | "typings": "typings", 9 | "postinstall": "typings install" 10 | }, 11 | "license": "ISC", 12 | "dependencies": { 13 | "@angular/common": "2.0.0-rc.1", 14 | "@angular/compiler": "2.0.0-rc.1", 15 | "@angular/core": "2.0.0-rc.1", 16 | "@angular/http": "2.0.0-rc.1", 17 | "@angular/platform-browser": "2.0.0-rc.1", 18 | "@angular/platform-browser-dynamic": "2.0.0-rc.1", 19 | "@angular/router": "2.0.0-rc.1", 20 | "@angular/router-deprecated": "2.0.0-rc.1", 21 | "@angular/upgrade": "2.0.0-rc.1", 22 | "bootstrap": "3.3.7", 23 | "del": "^2.2.1", 24 | "es6-shim": "^0.35.0", 25 | "gulp": "3.9.1", 26 | "gulp-sourcemaps": "1.6.0", 27 | "gulp-typescript": "^2.8.0", 28 | "jquery": "3.0.0", 29 | "raven-js": "3.0.5", 30 | "reflect-metadata": "^0.1.3", 31 | "rxjs": "5.0.0-beta.6", 32 | "systemjs": "0.19.27", 33 | "zone.js": "^0.6.12", 34 | "graceful-fs": "^4.0.0", 35 | "tslint": "3.11.0" 36 | }, 37 | "devDependencies": { 38 | "concurrently": "^2.0.0", 39 | "gulp-tslint": "5.0.0", 40 | "gulp-util": "^3.0.7", 41 | "jasmine-core": "2.4.1", 42 | "jasmine-spec-reporter": "^2.4.0", 43 | "lite-server": "^2.2.0", 44 | "typescript": "1.8.10", 45 | "typings": "^0.8.1", 46 | "webpack": "^1.13.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /powerdns/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = 'powerdns.apps.Powerdns' 2 | -------------------------------------------------------------------------------- /powerdns/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class Powerdns(AppConfig): 5 | 6 | name = 'powerdns' 7 | verbose_name = 'PowerDNS' 8 | -------------------------------------------------------------------------------- /powerdns/fixtures/test.yaml: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | --- 3 | # Default reverse instance 4 | 5 | - 6 | model: powerdns.domaintemplate 7 | pk: 1 8 | fields: 9 | name: reverse 10 | 11 | - 12 | model: powerdns.recordtemplate 13 | pk: 1 14 | fields: 15 | type: SOA 16 | auth: true 17 | name: '{domain-name}' 18 | content: 'ns1.{domain-name}. hostmaster.{domain-name}. 0 43200 600 1209600 600' 19 | ttl: 3600 20 | domain_template: 1 21 | - 22 | model: powerdns.recordtemplate 23 | pk: 2 24 | fields: 25 | type: NS 26 | auth: true 27 | name: '{domain-name}' 28 | content: 'ns1.{domain-name}' 29 | ttl: 3600 30 | domain_template: 1 31 | 32 | 33 | # Example domain 34 | - fields: 35 | account: '' 36 | created: !!timestamp '2015-05-21 05:58:49.747219' 37 | last_check: null 38 | master: '' 39 | modified: !!timestamp '2015-05-21 05:58:49.747254' 40 | name: example.com 41 | notified_serial: null 42 | type: MASTER 43 | model: powerdns.domain 44 | pk: 1 45 | - fields: 46 | auth: true 47 | change_date: 1432206225 48 | content: ns1.example.com. hostmaster.example.com. 0 43200 600 1209600 600 49 | created: !!timestamp '2015-05-21 06:00:06.910745' 50 | disabled: false 51 | domain: 1 52 | modified: !!timestamp '2015-05-21 06:03:45.775133' 53 | name: example.com 54 | number: null 55 | ordername: null 56 | prio: null 57 | ttl: 3600 58 | type: SOA 59 | model: powerdns.record 60 | pk: 1 61 | - fields: 62 | auth: true 63 | change_date: 1432206218 64 | content: 192.168.1.11 65 | created: !!timestamp '2015-05-21 06:03:09.887652' 66 | disabled: false 67 | domain: 1 68 | modified: !!timestamp '2015-05-21 06:03:38.785414' 69 | name: www.example.com 70 | number: 3232235787 71 | ordername: null 72 | prio: null 73 | ttl: 3600 74 | type: A 75 | model: powerdns.record 76 | pk: 2 77 | - fields: 78 | auth: true 79 | change_date: 1432206210 80 | content: 192.168.1.12 81 | created: !!timestamp '2015-05-21 06:03:30.294415' 82 | disabled: false 83 | domain: 1 84 | modified: !!timestamp '2015-05-21 06:03:30.294481' 85 | name: blog.example.com 86 | number: 3232235788 87 | ordername: null 88 | prio: null 89 | ttl: 3600 90 | type: A 91 | model: powerdns.record 92 | pk: 3 93 | -------------------------------------------------------------------------------- /powerdns/forms.py: -------------------------------------------------------------------------------- 1 | from dal import autocomplete 2 | from django import forms 3 | 4 | from powerdns.models import ( 5 | CryptoKey, 6 | Domain, 7 | DomainMetadata, 8 | Record, 9 | RecordRequest, 10 | ) 11 | 12 | 13 | class RecordForm(forms.ModelForm): 14 | class Meta: 15 | model = Record 16 | fields = ( 17 | 'owner', 'service', 'domain', 'name', 'type', 'content', 'ttl', 18 | 'prio', 'auth', 'disabled', 'remarks', 'template', 19 | ) 20 | widgets = { 21 | 'owner': autocomplete.ModelSelect2( 22 | url='autocomplete:users' 23 | ), 24 | 'domain': autocomplete.ModelSelect2( 25 | url='autocomplete:domains' 26 | ), 27 | 'service': autocomplete.ModelSelect2( 28 | url='autocomplete:services' 29 | ), 30 | } 31 | 32 | 33 | class RecordRequestForm(forms.ModelForm): 34 | class Meta: 35 | model = RecordRequest 36 | fields = ( 37 | 'domain', 'key', 'last_change_json', 'record', 'state', 38 | 'target_auth', 'target_content', 'target_disabled', 'target_name', 39 | 'owner', 'target_prio', 'target_remarks', 'target_ttl', 40 | 'target_type', 41 | ) 42 | widgets = { 43 | 'target_owner': autocomplete.ModelSelect2( 44 | url='autocomplete:users' 45 | ), 46 | 'domain': autocomplete.ModelSelect2( 47 | url='autocomplete:domains' 48 | ), 49 | 'record': autocomplete.ModelSelect2( 50 | url='autocomplete:records' 51 | ), 52 | } 53 | 54 | 55 | class DomainForm(forms.ModelForm): 56 | class Meta: 57 | model = Domain 58 | fields = ( 59 | 'owner', 'service', 'name', 'master', 'last_check', 'type', 60 | 'account', 'remarks', 'template', 'reverse_template', 'auto_ptr', 61 | 'unrestricted', 'require_sec_acceptance', 'require_seo_acceptance', 62 | ) 63 | widgets = { 64 | 'owner': autocomplete.ModelSelect2( 65 | url='autocomplete:users' 66 | ), 67 | 'service': autocomplete.ModelSelect2( 68 | url='autocomplete:services' 69 | ), 70 | } 71 | 72 | 73 | class DomainMetadataForm(forms.ModelForm): 74 | class Meta: 75 | model = DomainMetadata 76 | fields = ('domain', 'kind', 'content') 77 | widgets = { 78 | 'domain': autocomplete.ModelSelect2( 79 | url='autocomplete:domains' 80 | ) 81 | } 82 | 83 | 84 | class CryptoKeyForm(forms.ModelForm): 85 | class Meta: 86 | model = CryptoKey 87 | fields = ('domain', 'flags', 'active', 'content',) 88 | widgets = { 89 | 'domain': autocomplete.ModelSelect2( 90 | url='autocomplete:domains' 91 | ) 92 | } 93 | -------------------------------------------------------------------------------- /powerdns/locale/en/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allegro/django-powerdns-dnssec/333bdc668b6cda1a2ff240efd814a1896a1a8e07/powerdns/locale/en/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /powerdns/locale/en/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2012-11-06 01:34+0100\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: admin.py:22 21 | msgid "domain class" 22 | msgstr "" 23 | 24 | #: admin.py:29 25 | msgid "domain:forward" 26 | msgstr "forward" 27 | 28 | #: admin.py:30 29 | msgid "domain:reverse" 30 | msgstr "reverse" 31 | 32 | #: models.py:47 33 | #, python-format 34 | msgid "%s is not allowed to end in a period!" 35 | msgstr "" 36 | 37 | #: models.py:57 38 | msgid "Enter a valid IPv6 address." 39 | msgstr "" 40 | 41 | #: models.py:70 models.py:104 42 | msgid "name" 43 | msgstr "" 44 | 45 | #: models.py:72 46 | msgid "master" 47 | msgstr "" 48 | 49 | #: models.py:74 50 | msgid "last check" 51 | msgstr "" 52 | 53 | #: models.py:76 models.py:111 54 | msgid "type" 55 | msgstr "" 56 | 57 | #: models.py:79 58 | msgid "notified serial" 59 | msgstr "" 60 | 61 | #: models.py:82 models.py:250 62 | msgid "account" 63 | msgstr "" 64 | 65 | #: models.py:87 models.py:102 models.py:265 models.py:281 66 | msgid "domain" 67 | msgstr "" 68 | 69 | #: models.py:88 70 | msgid "domains" 71 | msgstr "" 72 | 73 | #: models.py:106 74 | msgid "" 75 | "Actual name of a record. Must not end in a '.' and be fully qualified - it " 76 | "is not relative to the name of the domain!" 77 | msgstr "" 78 | 79 | #: models.py:112 80 | msgid "Record qtype" 81 | msgstr "" 82 | 83 | #: models.py:115 models.py:267 models.py:286 84 | msgid "content" 85 | msgstr "" 86 | 87 | #: models.py:117 88 | msgid "" 89 | "The 'right hand side' of a DNS record. For an A record, this is the IP " 90 | "address" 91 | msgstr "" 92 | 93 | #: models.py:121 94 | msgid "TTL" 95 | msgstr "" 96 | 97 | #: models.py:122 98 | msgid "TTL in seconds" 99 | msgstr "" 100 | 101 | #: models.py:125 102 | msgid "priority" 103 | msgstr "" 104 | 105 | #: models.py:126 106 | msgid "" 107 | "For MX records, this should be the priority of the mail exchanger specified" 108 | msgstr "" 109 | 110 | #: models.py:130 111 | msgid "change date" 112 | msgstr "" 113 | 114 | #: models.py:131 115 | msgid "" 116 | "Set automatically by the system to trigger SOA updates and slave " 117 | "notifications" 118 | msgstr "" 119 | 120 | #: models.py:135 121 | msgid "DNSSEC Order" 122 | msgstr "" 123 | 124 | #: models.py:138 125 | msgid "authoritative" 126 | msgstr "" 127 | 128 | #: models.py:139 129 | msgid "" 130 | "Should be set for data for which is itself authoritative, which includes the " 131 | "SOA record and our own NS records but not set for NS records which are used " 132 | "for delegation or any delegation related glue (A, AAAA) records" 133 | msgstr "" 134 | 135 | #: models.py:150 136 | msgid "record" 137 | msgstr "" 138 | 139 | #: models.py:151 140 | msgid "records" 141 | msgstr "" 142 | 143 | #: models.py:247 144 | msgid "IP" 145 | msgstr "" 146 | 147 | #: models.py:248 148 | msgid "name server" 149 | msgstr "" 150 | 151 | #: models.py:257 152 | msgid "supermaster" 153 | msgstr "" 154 | 155 | #: models.py:258 156 | msgid "supermasters" 157 | msgstr "" 158 | 159 | #: models.py:266 160 | msgid "kind" 161 | msgstr "" 162 | 163 | #: models.py:272 models.py:273 164 | msgid "domain metadata" 165 | msgstr "" 166 | 167 | #: models.py:284 168 | msgid "flags" 169 | msgstr "" 170 | 171 | #: models.py:285 172 | msgid "active" 173 | msgstr "" 174 | 175 | #: models.py:291 176 | msgid "crypto key" 177 | msgstr "" 178 | 179 | #: models.py:292 180 | msgid "crypto keys" 181 | msgstr "" 182 | -------------------------------------------------------------------------------- /powerdns/locale/pl/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allegro/django-powerdns-dnssec/333bdc668b6cda1a2ff240efd814a1896a1a8e07/powerdns/locale/pl/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /powerdns/locale/pl/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2012-11-06 01:34+0100\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " 20 | "|| n%100>=20) ? 1 : 2)\n" 21 | 22 | #: admin.py:22 23 | msgid "domain class" 24 | msgstr "klasa domeny" 25 | 26 | #: admin.py:29 27 | msgid "domain:forward" 28 | msgstr "Zwykłe" 29 | 30 | #: admin.py:30 31 | msgid "domain:reverse" 32 | msgstr "Wsteczne" 33 | 34 | #: models.py:47 35 | #, python-format 36 | msgid "%s is not allowed to end in a period!" 37 | msgstr "%s nie może kończyć się kropką!" 38 | 39 | #: models.py:57 40 | msgid "Enter a valid IPv6 address." 41 | msgstr "Wpisz poprawny adres IPv6." 42 | 43 | #: models.py:70 models.py:104 44 | msgid "name" 45 | msgstr "nazwa" 46 | 47 | #: models.py:72 48 | msgid "master" 49 | msgstr "master" 50 | 51 | #: models.py:74 52 | msgid "last check" 53 | msgstr "ostatnie sprawdzenie" 54 | 55 | #: models.py:76 models.py:111 56 | msgid "type" 57 | msgstr "typ" 58 | 59 | #: models.py:79 60 | msgid "notified serial" 61 | msgstr "notyfikowany nr seryjny" 62 | 63 | #: models.py:82 models.py:250 64 | msgid "account" 65 | msgstr "konto" 66 | 67 | #: models.py:87 models.py:102 models.py:265 models.py:281 68 | msgid "domain" 69 | msgstr "domena" 70 | 71 | #: models.py:88 72 | msgid "domains" 73 | msgstr "domeny" 74 | 75 | #: models.py:106 76 | msgid "" 77 | "Actual name of a record. Must not end in a '.' and be fully qualified - it " 78 | "is not relative to the name of the domain!" 79 | msgstr "" 80 | "Nazwa rekordu. Nie może kończyć się kropką i musi być pełni kwalifikowana - " 81 | "nie jest relatywna do nazwy domeny!" 82 | 83 | #: models.py:112 84 | msgid "Record qtype" 85 | msgstr "qtyp rekordu" 86 | 87 | #: models.py:115 models.py:267 models.py:286 88 | msgid "content" 89 | msgstr "treść" 90 | 91 | #: models.py:117 92 | msgid "" 93 | "The 'right hand side' of a DNS record. For an A record, this is the IP " 94 | "address" 95 | msgstr "'Prawa strona' rekordu DNS. Dla rekordów typu A, jest to adres IP" 96 | 97 | #: models.py:121 98 | msgid "TTL" 99 | msgstr "TTL" 100 | 101 | #: models.py:122 102 | msgid "TTL in seconds" 103 | msgstr "TTL w sekundach" 104 | 105 | #: models.py:125 106 | msgid "priority" 107 | msgstr "priorytet" 108 | 109 | #: models.py:126 110 | msgid "" 111 | "For MX records, this should be the priority of the mail exchanger specified" 112 | msgstr "" 113 | "Dla rekordów MX, powinien być to priorytet serwera poczty elektronicznej" 114 | 115 | #: models.py:130 116 | msgid "change date" 117 | msgstr "data zmiany" 118 | 119 | #: models.py:131 120 | msgid "" 121 | "Set automatically by the system to trigger SOA updates and slave " 122 | "notifications" 123 | msgstr "" 124 | "Ustawiane automatycznie przez system, by wymusić aktualizację SOA i serwerów " 125 | "slave" 126 | 127 | #: models.py:135 128 | msgid "DNSSEC Order" 129 | msgstr "" 130 | 131 | #: models.py:138 132 | msgid "authoritative" 133 | msgstr "autorytatywny" 134 | 135 | #: models.py:139 136 | msgid "" 137 | "Should be set for data for which is itself authoritative, which includes the " 138 | "SOA record and our own NS records but not set for NS records which are used " 139 | "for delegation or any delegation related glue (A, AAAA) records" 140 | msgstr "" 141 | "Powinien być oznaczony dla danych, dla których jest autorytatywny, w tym " 142 | "rekordu SOA i własnych rekordów NS, ale nie ustawiony dla rekordów NS " 143 | "używanych do delegacji lub rekordów klejących (A, AAAA) związanych z " 144 | "delegacją" 145 | 146 | #: models.py:150 147 | msgid "record" 148 | msgstr "rekord" 149 | 150 | #: models.py:151 151 | msgid "records" 152 | msgstr "rekordy" 153 | 154 | #: models.py:247 155 | msgid "IP" 156 | msgstr "IP" 157 | 158 | #: models.py:248 159 | msgid "name server" 160 | msgstr "serwer nazw" 161 | 162 | #: models.py:257 163 | msgid "supermaster" 164 | msgstr "supermaster" 165 | 166 | #: models.py:258 167 | msgid "supermasters" 168 | msgstr "supermastery" 169 | 170 | #: models.py:266 171 | msgid "kind" 172 | msgstr "rodzaj" 173 | 174 | #: models.py:272 models.py:273 175 | msgid "domain metadata" 176 | msgstr "metadane domeny" 177 | 178 | #: models.py:284 179 | msgid "flags" 180 | msgstr "flagi" 181 | 182 | #: models.py:285 183 | msgid "active" 184 | msgstr "aktywny" 185 | 186 | #: models.py:291 187 | msgid "crypto key" 188 | msgstr "klucz kryptograficzny" 189 | 190 | #: models.py:292 191 | msgid "crypto keys" 192 | msgstr "klucze kryptograficzne" 193 | -------------------------------------------------------------------------------- /powerdns/migrations/0002_auto_20150514_0638.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.db import models, migrations 4 | import django.core.validators 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('powerdns', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='record', 16 | name='disabled', 17 | field=models.BooleanField(verbose_name='Disabled', help_text='This field should not be used for actual DNS queries. Note - this field works for pdns >= 3.4.0', default=False), 18 | preserve_default=True, 19 | ), 20 | migrations.AlterField( 21 | model_name='domain', 22 | name='name', 23 | field=models.CharField(validators=[django.core.validators.RegexValidator('^[A-Za-z][A-Za-z1-9.-]*[A-Za-z1-9]$')], verbose_name='name', unique=True, max_length=255), 24 | ), 25 | migrations.AlterField( 26 | model_name='record', 27 | name='content', 28 | field=models.CharField(null=True, verbose_name='content', max_length=255, blank=True, help_text="The 'right hand side' of a DNS record. For an A record, this is the IP address"), 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /powerdns/migrations/0003_auto_20150527_0808.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.db import models, migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('powerdns', '0002_auto_20150514_0638'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='record', 15 | name='auth', 16 | field=models.NullBooleanField(help_text='Should be set for data for which is itself authoritative, which includes the SOA record and our own NS records but not set for NS records which are used for delegation or any delegation related glue (A, AAAA) records', verbose_name='authoritative', default=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /powerdns/migrations/0004_auto_20150528_0540.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.db import models, migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('powerdns', '0003_auto_20150527_0808'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='domain', 15 | name='remarks', 16 | field=models.TextField(blank=True, default=''), 17 | preserve_default=False, 18 | ), 19 | migrations.AddField( 20 | model_name='record', 21 | name='remarks', 22 | field=models.TextField(blank=True, default=''), 23 | preserve_default=False, 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /powerdns/migrations/0005_auto_20150609_0754.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('powerdns', '0004_auto_20150528_0540'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='DomainTemplate', 16 | fields=[ 17 | ('id', models.AutoField(serialize=False, verbose_name='ID', auto_created=True, primary_key=True)), 18 | ('name', models.CharField(unique=True, max_length=255)), 19 | ], 20 | options={ 21 | }, 22 | bases=(models.Model,), 23 | ), 24 | migrations.CreateModel( 25 | name='RecordTemplate', 26 | fields=[ 27 | ('id', models.AutoField(serialize=False, verbose_name='ID', auto_created=True, primary_key=True)), 28 | ('type', models.CharField(verbose_name='type', max_length=6, help_text='Record type', blank=True, choices=[('A', 'A'), ('AAAA', 'AAAA'), ('AFSDB', 'AFSDB'), ('CERT', 'CERT'), ('CNAME', 'CNAME'), ('DNSKEY', 'DNSKEY'), ('DS', 'DS'), ('HINFO', 'HINFO'), ('KEY', 'KEY'), ('LOC', 'LOC'), ('MX', 'MX'), ('NAPTR', 'NAPTR'), ('NS', 'NS'), ('NSEC', 'NSEC'), ('PTR', 'PTR'), ('RP', 'RP'), ('RRSIG', 'RRSIG'), ('SOA', 'SOA'), ('SPF', 'SPF'), ('SRV', 'SRV'), ('SSHFP', 'SSHFP'), ('TXT', 'TXT')], null=True)), 29 | ('name', models.CharField(verbose_name='name', max_length=255)), 30 | ('content', models.CharField(verbose_name='content', max_length=255)), 31 | ('ttl', models.PositiveIntegerField(blank=True, default=3600, null=True, verbose_name='TTL', help_text='TTL in seconds')), 32 | ('prio', models.PositiveIntegerField(blank=True, verbose_name='priority', null=True, help_text='For MX records, this should be the priority of the mail exchanger specified')), 33 | ('auth', models.NullBooleanField(default=True, verbose_name='authoritative', help_text='Should be set for data for which is itself authoritative, which includes the SOA record and our own NS records but not set for NS records which are used for delegation or any delegation related glue (A, AAAA) records')), 34 | ('remarks', models.TextField(blank=True)), 35 | ('domain_template', models.ForeignKey(to='powerdns.DomainTemplate')), 36 | ], 37 | options={ 38 | }, 39 | bases=(models.Model,), 40 | ), 41 | migrations.AddField( 42 | model_name='domain', 43 | name='template', 44 | field=models.ForeignKey(to='powerdns.DomainTemplate', blank=True, null=True), 45 | preserve_default=True, 46 | ), 47 | migrations.AddField( 48 | model_name='record', 49 | name='template', 50 | field=models.ForeignKey(to='powerdns.RecordTemplate', blank=True, null=True), 51 | preserve_default=True, 52 | ), 53 | ] 54 | -------------------------------------------------------------------------------- /powerdns/migrations/0006_auto_20150612_0756.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('powerdns', '0005_auto_20150609_0754'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='domain', 16 | name='reverse_template', 17 | field=models.ForeignKey(blank=True, help_text='A template that should be used for reverse domains when PTR templates are automatically created for A records in this template.', to='powerdns.DomainTemplate', null=True, verbose_name='Reverse template', related_name='reverse_template_for'), 18 | preserve_default=True, 19 | ), 20 | migrations.AddField( 21 | model_name='record', 22 | name='auto_ptr', 23 | field=models.IntegerField(verbose_name='Auto PTR field', default=2), 24 | preserve_default=True, 25 | ), 26 | migrations.AddField( 27 | model_name='record', 28 | name='depends_on', 29 | field=models.ForeignKey(blank=True, help_text='This record is maintained automatically for another record. It should be automatically updated/deleted. Used for PTR recordsthat depend on A records.', to='powerdns.Record', null=True, verbose_name='Dependent on'), 30 | preserve_default=True, 31 | ), 32 | migrations.AlterField( 33 | model_name='domain', 34 | name='remarks', 35 | field=models.TextField(verbose_name='Additional remarks', blank=True), 36 | ), 37 | migrations.AlterField( 38 | model_name='domain', 39 | name='template', 40 | field=models.ForeignKey(blank=True, to='powerdns.DomainTemplate', null=True, verbose_name='Template'), 41 | ), 42 | migrations.AlterField( 43 | model_name='domaintemplate', 44 | name='name', 45 | field=models.CharField(verbose_name='Name', max_length=255, unique=True), 46 | ), 47 | migrations.AlterField( 48 | model_name='record', 49 | name='template', 50 | field=models.ForeignKey(blank=True, to='powerdns.RecordTemplate', null=True, verbose_name='Template'), 51 | ), 52 | migrations.AlterField( 53 | model_name='recordtemplate', 54 | name='domain_template', 55 | field=models.ForeignKey(verbose_name='Domain template', to='powerdns.DomainTemplate'), 56 | ), 57 | migrations.AlterField( 58 | model_name='recordtemplate', 59 | name='remarks', 60 | field=models.TextField(verbose_name='Additional remarks', blank=True), 61 | ), 62 | ] 63 | -------------------------------------------------------------------------------- /powerdns/migrations/0007_auto_20150710_0507.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | from django.conf import settings 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ('powerdns', '0006_auto_20150612_0756'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='domain', 18 | name='owner', 19 | field=models.ForeignKey(to=settings.AUTH_USER_MODEL, null=True), 20 | ), 21 | migrations.AddField( 22 | model_name='record', 23 | name='owner', 24 | field=models.ForeignKey(to=settings.AUTH_USER_MODEL, null=True), 25 | ), 26 | migrations.AlterField( 27 | model_name='cryptokey', 28 | name='modified', 29 | field=models.DateTimeField(verbose_name='last modified', auto_now=True), 30 | ), 31 | migrations.AlterField( 32 | model_name='domain', 33 | name='modified', 34 | field=models.DateTimeField(verbose_name='last modified', auto_now=True), 35 | ), 36 | migrations.AlterField( 37 | model_name='domainmetadata', 38 | name='modified', 39 | field=models.DateTimeField(verbose_name='last modified', auto_now=True), 40 | ), 41 | migrations.AlterField( 42 | model_name='record', 43 | name='modified', 44 | field=models.DateTimeField(verbose_name='last modified', auto_now=True), 45 | ), 46 | migrations.AlterField( 47 | model_name='supermaster', 48 | name='modified', 49 | field=models.DateTimeField(verbose_name='last modified', auto_now=True), 50 | ), 51 | ] 52 | -------------------------------------------------------------------------------- /powerdns/migrations/0008_auto_20150821_0630.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | from django.conf import settings 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('powerdns', '0007_auto_20150710_0507'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='recordtemplate', 17 | name='auto_ptr', 18 | field=models.IntegerField(default=2, verbose_name='Auto PTR field'), 19 | ), 20 | migrations.AlterField( 21 | model_name='domain', 22 | name='owner', 23 | field=models.ForeignKey(null=True, blank=True, to=settings.AUTH_USER_MODEL), 24 | ), 25 | migrations.AlterField( 26 | model_name='record', 27 | name='owner', 28 | field=models.ForeignKey(null=True, blank=True, to=settings.AUTH_USER_MODEL), 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /powerdns/migrations/0009_auto_20150915_0342.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import powerdns.utils 6 | import django.core.validators 7 | import dj.choices.fields 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | ('powerdns', '0008_auto_20150821_0630'), 14 | ] 15 | 16 | operations = [ 17 | migrations.AlterField( 18 | model_name='domain', 19 | name='name', 20 | field=models.CharField(unique=True, verbose_name='name', max_length=255, validators=[django.core.validators.RegexValidator('^([A-Za-z0-9-]+\\.)*([A-Za-z0-9])+$')]), 21 | ), 22 | migrations.AlterField( 23 | model_name='record', 24 | name='auto_ptr', 25 | field=dj.choices.fields.ChoiceField(default=2, choices=powerdns.utils.AutoPtrOptions, verbose_name='Auto PTR record'), 26 | ), 27 | migrations.AlterField( 28 | model_name='recordtemplate', 29 | name='auto_ptr', 30 | field=dj.choices.fields.ChoiceField(default=2, choices=powerdns.utils.AutoPtrOptions, verbose_name='Auto PTR field'), 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /powerdns/migrations/0010_auto_20150921_0613.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import dj.choices.fields 6 | import powerdns.utils 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('powerdns', '0009_auto_20150915_0342'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='domain', 18 | name='record_auto_ptr', 19 | field=dj.choices.fields.ChoiceField(default=2, choices=powerdns.utils.AutoPtrOptions), 20 | ), 21 | migrations.AddField( 22 | model_name='domaintemplate', 23 | name='record_auto_ptr', 24 | field=dj.choices.fields.ChoiceField(default=2, choices=powerdns.utils.AutoPtrOptions), 25 | ), 26 | migrations.AddField( 27 | model_name='domaintemplate', 28 | name='type', 29 | field=models.CharField(null=True, blank=True, verbose_name='type', choices=[('MASTER', 'MASTER'), ('NATIVE', 'NATIVE'), ('SLAVE', 'SLAVE')], max_length=6, help_text='Record type'), 30 | ), 31 | migrations.AlterField( 32 | model_name='domaintemplate', 33 | name='name', 34 | field=models.CharField(max_length=255, verbose_name='Template identifier', unique=True), 35 | ), 36 | ] 37 | -------------------------------------------------------------------------------- /powerdns/migrations/0011_auto_20151103_0546.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | from django.conf import settings 6 | import django.core.validators 7 | import powerdns.models.powerdns 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ('contenttypes', '0002_remove_content_type_name'), 15 | ('powerdns', '0010_auto_20150921_0613'), 16 | ] 17 | 18 | operations = [ 19 | migrations.CreateModel( 20 | name='Authorisation', 21 | fields=[ 22 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 23 | ('target_id', models.PositiveIntegerField()), 24 | ('authorised', models.ForeignKey(related_name='received_authorisations', to=settings.AUTH_USER_MODEL)), 25 | ('content_type', models.ForeignKey(to='contenttypes.ContentType')), 26 | ('owner', models.ForeignKey(related_name='issued_authorisations', to=settings.AUTH_USER_MODEL)), 27 | ], 28 | ), 29 | migrations.AlterField( 30 | model_name='domain', 31 | name='name', 32 | field=models.CharField(verbose_name='name', unique=True, max_length=255, validators=[django.core.validators.RegexValidator('^(\\*\\.)?([_A-Za-z0-9-]+\\.)*([A-Za-z0-9])+$'), powerdns.models.powerdns.SubDomainValidator()]), 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /powerdns/migrations/0012_auto_20151112_0504.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import powerdns.models.powerdns 6 | import django.core.validators 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('powerdns', '0011_auto_20151103_0546'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='domain', 18 | name='name', 19 | field=models.CharField(verbose_name='name', max_length=255, validators=[django.core.validators.RegexValidator('^(\\*\\.)?([_A-Za-z0-9-]+\\.)*([A-Za-z0-9])+$'), powerdns.models.powerdns.SubDomainValidator()], unique=True), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /powerdns/migrations/0014_auto_20151124_0505.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import django.core.validators 6 | import powerdns.models.powerdns 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('powerdns', '0013_auto_20151124_0301'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='domain', 18 | name='name', 19 | field=models.CharField(max_length=255, validators=[django.core.validators.RegexValidator('^(\\*\\.)?([_A-Za-z0-9-]+\\.)*([A-Za-z0-9])+$'), powerdns.models.powerdns.SubDomainValidator()], verbose_name='name', unique=True), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /powerdns/migrations/0015_auto_20151214_0632.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import django.core.validators 6 | import powerdns.models.powerdns 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('powerdns', '0014_auto_20151124_0505'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='domain', 18 | name='unrestricted', 19 | field=models.BooleanField(default=False, verbose_name='Unrestricted', help_text="Can users that are not owners of this domain add recordsto it without owner's permission?"), 20 | ), 21 | migrations.AddField( 22 | model_name='domainrequest', 23 | name='unrestricted', 24 | field=models.BooleanField(default=False, verbose_name='Unrestricted', help_text="Can users that are not owners of this domain add recordsto it without owner's permission?"), 25 | ), 26 | migrations.AddField( 27 | model_name='domaintemplate', 28 | name='unrestricted', 29 | field=models.BooleanField(default=False, verbose_name='Unrestricted', help_text="Can users that are not owners of this domain add recordsto it without owner's permission?"), 30 | ), 31 | migrations.AlterField( 32 | model_name='domain', 33 | name='name', 34 | field=models.CharField(max_length=255, validators=[django.core.validators.RegexValidator('^(\\*\\.)?([_A-Za-z0-9-]+\\.)*([A-Za-z0-9])+$'), powerdns.models.powerdns.SubDomainValidator()], unique=True, verbose_name='name'), 35 | ), 36 | ] 37 | -------------------------------------------------------------------------------- /powerdns/migrations/0016_auto_20151217_0817.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import powerdns.models.powerdns 6 | import django.core.validators 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('powerdns', '0015_auto_20151214_0632'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='domain', 18 | name='name', 19 | field=models.CharField(unique=True, validators=[django.core.validators.RegexValidator('^(\\*\\.)?([_A-Za-z0-9-]+\\.)*([A-Za-z0-9])+$'), powerdns.models.powerdns.SubDomainValidator()], max_length=255, verbose_name='name'), 20 | ), 21 | migrations.AlterField( 22 | model_name='record', 23 | name='name', 24 | field=models.CharField(help_text="Actual name of a record. Must not end in a '.' and be fully qualified - it is not relative to the name of the domain!", validators=[django.core.validators.RegexValidator('^(\\*\\.)?([_A-Za-z0-9-]+\\.)*([A-Za-z0-9])+$')], default='fixme', max_length=255, verbose_name='name'), 25 | preserve_default=False, 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /powerdns/migrations/0017_auto_20160104_0642.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import django.core.validators 6 | import powerdns.models.powerdns 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('powerdns', '0016_auto_20151217_0817'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='domain', 18 | name='name', 19 | field=models.CharField(unique=True, max_length=255, validators=[django.core.validators.RegexValidator('^(\\*\\.)?([_A-Za-z0-9-]+\\.)*([A-Za-z0-9])+$'), powerdns.models.powerdns.SubDomainValidator()], verbose_name='name'), 20 | ), 21 | migrations.AlterField( 22 | model_name='recordrequest', 23 | name='name', 24 | field=models.CharField(verbose_name='name', help_text="Actual name of a record. Must not end in a '.' and be fully qualified - it is not relative to the name of the domain!", max_length=255, validators=[django.core.validators.RegexValidator('^(\\*\\.)?([_A-Za-z0-9-]+\\.)*([A-Za-z0-9])+$')], default='unknown.com'), 25 | preserve_default=False, 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /powerdns/migrations/0018_auto_20160105_0824.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import powerdns.models.powerdns 6 | import django.core.validators 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('powerdns', '0017_auto_20160104_0642'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='domain', 18 | name='name', 19 | field=models.CharField(validators=[django.core.validators.RegexValidator('^(\\*\\.)?([_A-Za-z0-9-]+\\.)*([A-Za-z0-9])+$'), powerdns.models.powerdns.SubDomainValidator()], verbose_name='name', max_length=255, unique=True), 20 | ), 21 | migrations.AlterField( 22 | model_name='recordrequest', 23 | name='type', 24 | field=models.CharField(verbose_name='type', default='TXT', choices=[('A', 'A'), ('AAAA', 'AAAA'), ('AFSDB', 'AFSDB'), ('CERT', 'CERT'), ('CNAME', 'CNAME'), ('DNSKEY', 'DNSKEY'), ('DS', 'DS'), ('HINFO', 'HINFO'), ('KEY', 'KEY'), ('LOC', 'LOC'), ('MX', 'MX'), ('NAPTR', 'NAPTR'), ('NS', 'NS'), ('NSEC', 'NSEC'), ('PTR', 'PTR'), ('RP', 'RP'), ('RRSIG', 'RRSIG'), ('SOA', 'SOA'), ('SPF', 'SPF'), ('SRV', 'SRV'), ('SSHFP', 'SSHFP'), ('TXT', 'TXT')], max_length=6, help_text='Record qtype'), 25 | preserve_default=False, 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /powerdns/migrations/0019_auto_20160111_0842.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | from django.conf import settings 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ('powerdns', '0018_auto_20160105_0824'), 13 | ] 14 | 15 | operations = [ 16 | migrations.RenameField( 17 | model_name='domainrequest', 18 | old_name='account', 19 | new_name='target_account', 20 | ), 21 | migrations.RenameField( 22 | model_name='domainrequest', 23 | old_name='master', 24 | new_name='target_master', 25 | ), 26 | migrations.RenameField( 27 | model_name='domainrequest', 28 | old_name='name', 29 | new_name='target_name', 30 | ), 31 | migrations.RenameField( 32 | model_name='domainrequest', 33 | old_name='record_auto_ptr', 34 | new_name='target_record_auto_ptr', 35 | ), 36 | migrations.RenameField( 37 | model_name='domainrequest', 38 | old_name='remarks', 39 | new_name='target_remarks', 40 | ), 41 | migrations.RenameField( 42 | model_name='domainrequest', 43 | old_name='reverse_template', 44 | new_name='target_reverse_template', 45 | ), 46 | migrations.RenameField( 47 | model_name='domainrequest', 48 | old_name='template', 49 | new_name='target_template', 50 | ), 51 | migrations.RenameField( 52 | model_name='domainrequest', 53 | old_name='type', 54 | new_name='target_type', 55 | ), 56 | migrations.RenameField( 57 | model_name='domainrequest', 58 | old_name='unrestricted', 59 | new_name='target_unrestricted', 60 | ), 61 | migrations.RenameField( 62 | model_name='recordrequest', 63 | old_name='auth', 64 | new_name='target_auth', 65 | ), 66 | migrations.RenameField( 67 | model_name='recordrequest', 68 | old_name='content', 69 | new_name='target_content', 70 | ), 71 | migrations.RenameField( 72 | model_name='recordrequest', 73 | old_name='disabled', 74 | new_name='target_disabled', 75 | ), 76 | migrations.RenameField( 77 | model_name='recordrequest', 78 | old_name='name', 79 | new_name='target_name', 80 | ), 81 | migrations.RenameField( 82 | model_name='recordrequest', 83 | old_name='ordername', 84 | new_name='target_ordername', 85 | ), 86 | migrations.RenameField( 87 | model_name='recordrequest', 88 | old_name='prio', 89 | new_name='target_prio', 90 | ), 91 | migrations.RenameField( 92 | model_name='recordrequest', 93 | old_name='remarks', 94 | new_name='target_remarks', 95 | ), 96 | migrations.RenameField( 97 | model_name='recordrequest', 98 | old_name='ttl', 99 | new_name='target_ttl', 100 | ), 101 | migrations.RenameField( 102 | model_name='recordrequest', 103 | old_name='type', 104 | new_name='target_type', 105 | ), 106 | migrations.AddField( 107 | model_name='domainrequest', 108 | name='target_owner', 109 | field=models.ForeignKey(null=True, verbose_name='Owner', related_name='+', to=settings.AUTH_USER_MODEL), 110 | ), 111 | migrations.AddField( 112 | model_name='recordrequest', 113 | name='target_owner', 114 | field=models.ForeignKey(null=True, verbose_name='Owner', related_name='+', to=settings.AUTH_USER_MODEL), 115 | ), 116 | ] 117 | -------------------------------------------------------------------------------- /powerdns/migrations/0020_remove_recordrequest_target_ordername.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('powerdns', '0019_auto_20160111_0842'), 11 | ] 12 | 13 | operations = [ 14 | migrations.RemoveField( 15 | model_name='recordrequest', 16 | name='target_ordername', 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /powerdns/migrations/0021_tsigkey.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('powerdns', '0020_remove_recordrequest_target_ordername'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='TsigKey', 16 | fields=[ 17 | ('id', models.AutoField(serialize=False, auto_created=True, verbose_name='ID', primary_key=True)), 18 | ('name', models.CharField(help_text='Key name', max_length=255, verbose_name='name')), 19 | ('algorithm', models.CharField(max_length=50, verbose_name='algorithm', choices=[('hmac-md5', 'hmac-md5')])), 20 | ('secret', models.CharField(help_text='Secret key', max_length=255, verbose_name='secret')), 21 | ], 22 | options={ 23 | 'db_table': 'tsigkeys', 24 | }, 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /powerdns/migrations/0022_auto_20160531_0843.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('powerdns', '0021_tsigkey'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='recordrequest', 17 | name='record', 18 | field=models.ForeignKey(help_text='The record for which a change is being requested', blank=True, on_delete=django.db.models.deletion.DO_NOTHING, to='powerdns.Record', related_name='requests', db_constraint=False, null=True), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /powerdns/migrations/0023_domaintemplate_is_public_domain.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('powerdns', '0022_auto_20160531_0843'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='domaintemplate', 16 | name='is_public_domain', 17 | field=models.BooleanField(default=True), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /powerdns/migrations/0024_auto_20160627_0658.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import django_extensions.db.fields.json 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('powerdns', '0023_domaintemplate_is_public_domain'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='deleterequest', 17 | name='last_change_json', 18 | field=django_extensions.db.fields.json.JSONField(null=True, blank=True), 19 | ), 20 | migrations.AddField( 21 | model_name='domainrequest', 22 | name='last_change_json', 23 | field=django_extensions.db.fields.json.JSONField(null=True, blank=True), 24 | ), 25 | migrations.AddField( 26 | model_name='recordrequest', 27 | name='last_change_json', 28 | field=django_extensions.db.fields.json.JSONField(null=True, blank=True), 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /powerdns/migrations/0025_auto_20160822_0137.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('powerdns', '0024_auto_20160627_0658'), 11 | ] 12 | 13 | operations = [ 14 | migrations.RenameField( 15 | model_name='domain', 16 | old_name='record_auto_ptr', 17 | new_name='auto_ptr', 18 | ), 19 | migrations.RenameField( 20 | model_name='domainrequest', 21 | old_name='target_record_auto_ptr', 22 | new_name='target_auto_ptr', 23 | ), 24 | migrations.RenameField( 25 | model_name='domaintemplate', 26 | old_name='record_auto_ptr', 27 | new_name='auto_ptr', 28 | ), 29 | migrations.RemoveField( 30 | model_name='record', 31 | name='auto_ptr', 32 | ), 33 | migrations.RemoveField( 34 | model_name='recordtemplate', 35 | name='auto_ptr', 36 | ), 37 | ] 38 | -------------------------------------------------------------------------------- /powerdns/migrations/0026_auto_20160829_0337.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('powerdns', '0025_auto_20160822_0137'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='record', 16 | name='type', 17 | field=models.CharField(help_text='Record qtype', verbose_name='type', max_length=6, null=True, choices=[('A', 'A'), ('AAAA', 'AAAA'), ('AFSDB', 'AFSDB'), ('CERT', 'CERT'), ('CNAME', 'CNAME'), ('DNSKEY', 'DNSKEY'), ('DS', 'DS'), ('HINFO', 'HINFO'), ('KEY', 'KEY'), ('LOC', 'LOC'), ('MX', 'MX'), ('NAPTR', 'NAPTR'), ('NS', 'NS'), ('NSEC', 'NSEC'), ('PTR', 'PTR'), ('RP', 'RP'), ('RRSIG', 'RRSIG'), ('SOA', 'SOA'), ('SPF', 'SPF'), ('SRV', 'SRV'), ('SSHFP', 'SSHFP'), ('TXT', 'TXT')]), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /powerdns/migrations/0027_auto_20160915_0349.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | from django.conf import settings 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ('powerdns', '0026_auto_20160829_0337'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Service', 18 | fields=[ 19 | ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')), 20 | ('created', models.DateTimeField(auto_now_add=True, verbose_name='date created')), 21 | ('modified', models.DateTimeField(auto_now=True, verbose_name='last modified')), 22 | ('name', models.CharField(max_length=255, verbose_name='name')), 23 | ('uid', models.CharField(max_length=100, db_index=True, unique=True)), 24 | ('status', models.CharField(max_length=100, db_index=True, choices=[('ACTIVE', 'Active'), ('OBSOLETE', 'Obsolete'), ('PENDING_OBSOLESCENCE', 'Pending Obsolescence'), ('PLANNING', 'Planning'), ('PENDING', 'Pending')])), 25 | ], 26 | options={ 27 | 'abstract': False, 28 | }, 29 | ), 30 | migrations.CreateModel( 31 | name='ServiceOwner', 32 | fields=[ 33 | ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')), 34 | ('created', models.DateTimeField(auto_now_add=True, verbose_name='date created')), 35 | ('modified', models.DateTimeField(auto_now=True, verbose_name='last modified')), 36 | ('ownership_type', models.CharField(max_length=10, db_index=True, choices=[('BO', 'Business Owner'), ('TO', 'Technical Owner')])), 37 | ('service', models.ForeignKey(to='powerdns.Service')), 38 | ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)), 39 | ], 40 | options={ 41 | 'abstract': False, 42 | }, 43 | ), 44 | migrations.AddField( 45 | model_name='service', 46 | name='owners', 47 | field=models.ManyToManyField(through='powerdns.ServiceOwner', to=settings.AUTH_USER_MODEL), 48 | ), 49 | migrations.AddField( 50 | model_name='domain', 51 | name='service', 52 | field=models.ForeignKey(blank=True, null=True, to='powerdns.Service'), 53 | ), 54 | migrations.AddField( 55 | model_name='record', 56 | name='service', 57 | field=models.ForeignKey(blank=True, null=True, to='powerdns.Service'), 58 | ), 59 | ] 60 | -------------------------------------------------------------------------------- /powerdns/migrations/0028_auto_20160901_0235.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('powerdns', '0027_auto_20160915_0349'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='record', 16 | name='number', 17 | field=models.DecimalField(default=None, decimal_places=0, max_digits=39, db_index=True, blank=True, verbose_name='IP number', null=True, editable=False), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /powerdns/migrations/0029_auto_20160922_0757.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('powerdns', '0028_auto_20160901_0235'), 11 | ] 12 | 13 | operations = [ 14 | migrations.RemoveField( 15 | model_name='service', 16 | name='status', 17 | ), 18 | migrations.AddField( 19 | model_name='service', 20 | name='is_active', 21 | field=models.BooleanField(default=True), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /powerdns/migrations/0030_auto_20161026_0944.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | from django.conf import settings 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ('powerdns', '0029_auto_20160922_0757'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='DomainOwner', 18 | fields=[ 19 | ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')), 20 | ('ownership_type', models.CharField(max_length=10, choices=[('BO', 'Business Owner'), ('TO', 'Technical Owner')], db_index=True)), 21 | ('domain', models.ForeignKey(to='powerdns.Domain')), 22 | ('owner', models.ForeignKey(to=settings.AUTH_USER_MODEL)), 23 | ], 24 | ), 25 | migrations.RenameField( 26 | model_name='serviceowner', 27 | old_name='user', 28 | new_name='owner', 29 | ), 30 | migrations.AlterField( 31 | model_name='service', 32 | name='owners', 33 | field=models.ManyToManyField(to=settings.AUTH_USER_MODEL, related_name='service_owners', through='powerdns.ServiceOwner'), 34 | ), 35 | migrations.AddField( 36 | model_name='domain', 37 | name='direct_owners', 38 | field=models.ManyToManyField(to=settings.AUTH_USER_MODEL, related_name='domain_owners', through='powerdns.DomainOwner'), 39 | ), 40 | ] 41 | -------------------------------------------------------------------------------- /powerdns/migrations/0031_auto_20161102_0640.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('powerdns', '0030_auto_20161026_0944'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='domainrequest', 16 | name='target_service', 17 | field=models.ForeignKey(blank=True, null=True, to='powerdns.Service'), 18 | ), 19 | migrations.AddField( 20 | model_name='recordrequest', 21 | name='target_service', 22 | field=models.ForeignKey(blank=True, null=True, to='powerdns.Service'), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /powerdns/migrations/0032_auto_20161102_2006.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | from django.utils import timezone 6 | import datetime 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('powerdns', '0031_auto_20161102_0640'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='deleterequest', 18 | name='created', 19 | field=models.DateTimeField(auto_now_add=True, verbose_name='date created', default=datetime.datetime(1970, 1, 1, 0, 0, tzinfo=timezone.utc)), 20 | preserve_default=False, 21 | ), 22 | migrations.AddField( 23 | model_name='deleterequest', 24 | name='modified', 25 | field=models.DateTimeField(auto_now=True, verbose_name='last modified', default=datetime.datetime(1970, 1, 1, 0, 0, tzinfo=timezone.utc)), 26 | preserve_default=False, 27 | ), 28 | migrations.AddField( 29 | model_name='domainrequest', 30 | name='created', 31 | field=models.DateTimeField(auto_now_add=True, verbose_name='date created', default=datetime.datetime(1970, 1, 1, 0, 0, tzinfo=timezone.utc)), 32 | preserve_default=False, 33 | ), 34 | migrations.AddField( 35 | model_name='domainrequest', 36 | name='modified', 37 | field=models.DateTimeField(auto_now=True, verbose_name='last modified', default=datetime.datetime(1970, 1, 1, 0, 0, tzinfo=timezone.utc)), 38 | preserve_default=False, 39 | ), 40 | migrations.AddField( 41 | model_name='recordrequest', 42 | name='created', 43 | field=models.DateTimeField(auto_now_add=True, verbose_name='date created', default=datetime.datetime(1970, 1, 1, 0, 0, tzinfo=timezone.utc)), 44 | preserve_default=False, 45 | ), 46 | migrations.AddField( 47 | model_name='recordrequest', 48 | name='modified', 49 | field=models.DateTimeField(auto_now=True, verbose_name='last modified', default=datetime.datetime(1970, 1, 1, 0, 0, tzinfo=timezone.utc)), 50 | preserve_default=False, 51 | ), 52 | ] 53 | -------------------------------------------------------------------------------- /powerdns/migrations/0033_auto_20161114_1442.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('powerdns', '0032_auto_20161102_2006'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterUniqueTogether( 15 | name='domainowner', 16 | unique_together=set([('domain', 'owner', 'ownership_type')]), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /powerdns/migrations/0034_auto_20161115_1452.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('powerdns', '0033_auto_20161114_1442'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='domain', 16 | name='require_sec_acceptance', 17 | field=models.BooleanField(help_text='Do new A records require security acceptance', default=False), 18 | ), 19 | migrations.AddField( 20 | model_name='domain', 21 | name='require_seo_acceptance', 22 | field=models.BooleanField(help_text='Does deleting A records require SEO acceptance', default=False), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /powerdns/migrations/0035_auto_20161228_0058.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.4 on 2016-12-28 07:58 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('powerdns', '0034_auto_20161115_1452'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='domainmetadata', 17 | name='kind', 18 | field=models.CharField(max_length=31, verbose_name='kind'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /powerdns/migrations/0036_auto_20171128_0119.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | import django.core.validators 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('powerdns', '0035_auto_20161228_0058'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RemoveField( 16 | model_name='authorisation', 17 | name='authorised', 18 | ), 19 | migrations.RemoveField( 20 | model_name='authorisation', 21 | name='content_type', 22 | ), 23 | migrations.RemoveField( 24 | model_name='authorisation', 25 | name='owner', 26 | ), 27 | migrations.AlterField( 28 | model_name='domain', 29 | name='name', 30 | field=models.CharField(verbose_name='name', unique=True, validators=[django.core.validators.RegexValidator('^(\\*\\.)?([_A-Za-z0-9-]+\\.)*([A-Za-z0-9])+$')], max_length=255), 31 | ), 32 | migrations.DeleteModel( 33 | name='Authorisation', 34 | ), 35 | ] 36 | -------------------------------------------------------------------------------- /powerdns/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allegro/django-powerdns-dnssec/333bdc668b6cda1a2ff240efd814a1896a1a8e07/powerdns/migrations/__init__.py -------------------------------------------------------------------------------- /powerdns/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .ownership import * # noqa 2 | from .powerdns import * # noqa 3 | from .requests import * # noqa 4 | from .templates import * # noqa 5 | from .tsigkeys import * # noqa 6 | -------------------------------------------------------------------------------- /powerdns/models/ownership.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module provide a new way of ownership/permission to Domains and Records. 3 | 4 | The main concept of this is: 5 | - all Domains and Records belongs to a Service 6 | - Service has owners 7 | - Permissions to adding/editing/deleting Domains and Records comes from 8 | this ownership 9 | """ 10 | 11 | from enum import Enum 12 | 13 | from django.conf import settings 14 | from django.db import models 15 | from django.utils.translation import ugettext_lazy as _ 16 | 17 | from ..utils import TimeTrackable 18 | 19 | 20 | class OwnershipType(Enum): 21 | BO = 'Business Owner' 22 | TO = 'Technical Owner' 23 | 24 | 25 | class Service(TimeTrackable): 26 | name = models.CharField(_("name"), max_length=255) 27 | uid = models.CharField(max_length=100, unique=True, db_index=True) 28 | is_active = models.BooleanField(null=False, default=True) 29 | owners = models.ManyToManyField( 30 | settings.AUTH_USER_MODEL, through='ServiceOwner', 31 | related_name='service_owners', 32 | ) 33 | 34 | def __str__(self): 35 | return '{} ({})'.format(self.name, self.uid) 36 | 37 | @classmethod 38 | def get_service_by_uid(cls, uid): 39 | """ 40 | Return service by UID or None If service with specified UID does not 41 | exist. 42 | """ 43 | 44 | try: 45 | service = cls.objects.get(uid=uid) 46 | except cls.DoesNotExist: 47 | return 48 | else: 49 | return service 50 | 51 | 52 | class ServiceOwner(TimeTrackable): 53 | service = models.ForeignKey(Service) 54 | owner = models.ForeignKey(settings.AUTH_USER_MODEL) 55 | ownership_type = models.CharField( 56 | max_length=10, db_index=True, 57 | choices=[(type_.name, type_.value) for type_ in OwnershipType], 58 | ) 59 | 60 | def __str__(self): 61 | return '{} - {} ({})'.format( 62 | self.owner, self.service, self.ownership_type, 63 | ) 64 | 65 | 66 | class OwnershipByService(models.Model): 67 | 68 | class Meta: 69 | abstract = True 70 | 71 | service = models.ForeignKey(Service, blank=True, null=True) 72 | 73 | def has_owner(self): 74 | has_service_owner = self.service and self.service.owners.exists() 75 | if not self.owner_id and not has_service_owner: 76 | return False 77 | return True 78 | 79 | @property 80 | def service_owners(self): 81 | if self.service: 82 | owners = self.service.serviceowner_set 83 | else: 84 | owners = ServiceOwner.objects.none() 85 | return owners 86 | 87 | @property 88 | def business_service_owners(self): 89 | return self.service_owners.filter( 90 | ownership_type=OwnershipType.BO.name 91 | ) 92 | 93 | @property 94 | def technical_service_owners(self): 95 | return self.service_owners.filter( 96 | ownership_type=OwnershipType.TO.name 97 | ) 98 | 99 | def _has_access_by_service(self, user): 100 | "Check if user is one of owners of service assigned to this model." 101 | if self.service: 102 | permission_by_service = ( 103 | user.id in self.service_owners.values_list( 104 | 'owner_id', flat=True 105 | ) 106 | ) 107 | else: 108 | permission_by_service = False 109 | return permission_by_service 110 | -------------------------------------------------------------------------------- /powerdns/models/templates.py: -------------------------------------------------------------------------------- 1 | """Models and signal subscriptions for templating system""" 2 | 3 | from django.db import models 4 | from django.db.models.signals import post_save 5 | from django.dispatch import receiver 6 | from django.utils.translation import ugettext_lazy as _ 7 | from dj.choices.fields import ChoiceField 8 | 9 | from .powerdns import Domain, Record 10 | from ..utils import AutoPtrOptions 11 | 12 | 13 | class DomainTemplateManager(models.Manager): 14 | def get_by_natural_key(self, name): 15 | return self.get(name=name) 16 | 17 | 18 | class DomainTemplate(models.Model): 19 | """A predefined template containing several record templates""" 20 | 21 | copy_fields = ['auto_ptr', 'type'] 22 | 23 | name = models.CharField( 24 | _('Template identifier'), 25 | unique=True, 26 | max_length=255 27 | ) 28 | objects = DomainTemplateManager() 29 | type = models.CharField( 30 | _("type"), max_length=6, blank=True, null=True, 31 | choices=Domain.DOMAIN_TYPE, help_text=_("Record type"), 32 | ) 33 | unrestricted = models.BooleanField( 34 | _('Unrestricted'), 35 | null=False, 36 | default=False, 37 | help_text=_( 38 | "Can users that are not owners of this domain add records" 39 | "to it without owner's permission?" 40 | ) 41 | ) 42 | is_public_domain = models.BooleanField(default=True) 43 | 44 | auto_ptr = ChoiceField( 45 | choices=AutoPtrOptions, 46 | default=AutoPtrOptions.ALWAYS, 47 | ) 48 | 49 | def __str__(self): 50 | return self.name 51 | 52 | def natural_key(self): 53 | return (self.name,) 54 | 55 | 56 | class RecordTemplateManager(models.Manager): 57 | def get_by_natural_key(self, domain_name, type, name, content): 58 | return self.get( 59 | domain_template__name=domain_name, 60 | type=type, 61 | name=name, 62 | content=content, 63 | ) 64 | 65 | 66 | class RecordTemplate(models.Model): 67 | """A predefined record template that would cause a corresponding record 68 | to be created.""" 69 | objects = RecordTemplateManager() 70 | domain_template = models.ForeignKey( 71 | DomainTemplate, verbose_name=_('Domain template') 72 | ) 73 | type = models.CharField( 74 | _("type"), max_length=6, blank=True, null=True, 75 | choices=Record.RECORD_TYPE, help_text=_("Record type"), 76 | ) 77 | name = models.CharField(_('name'), max_length=255) 78 | content = models.CharField(_('content'), max_length=255) 79 | ttl = models.PositiveIntegerField( 80 | _("TTL"), blank=True, null=True, default=3600, 81 | help_text=_("TTL in seconds"), 82 | ) 83 | prio = models.PositiveIntegerField( 84 | _("priority"), blank=True, null=True, 85 | help_text=_("For MX records, this should be the priority of the" 86 | " mail exchanger specified"), 87 | ) 88 | auth = models.NullBooleanField( 89 | _("authoritative"), 90 | help_text=_("Should be set for data for which is itself" 91 | " authoritative, which includes the SOA record and our own" 92 | " NS records but not set for NS records which are used for" 93 | " delegation or any delegation related glue (A, AAAA)" 94 | " records"), 95 | default=True, 96 | ) 97 | remarks = models.TextField(_('Additional remarks'), blank=True) 98 | 99 | def natural_key(self): 100 | return (self.domain_template.name, self.type, self.name, self.content) 101 | 102 | def __str__(self): 103 | if self.prio is not None: 104 | content = "%d %s" % (self.prio, self.content) 105 | else: 106 | content = self.content 107 | return "%s IN %s %s" % (self.name, self.type, content) 108 | 109 | def get_kwargs(self, domain): 110 | kwargs = {} 111 | template_kwargs = { 112 | 'domain-name': domain.name, 113 | } 114 | for argname in ['type', 'ttl', 'prio', 'auth']: 115 | kwargs[argname] = getattr(self, argname) 116 | for argname in ['name', 'content']: 117 | kwargs[argname] = getattr(self, argname).format(**template_kwargs) 118 | kwargs['template'] = self 119 | kwargs['domain'] = domain 120 | return kwargs 121 | 122 | def create_record(self, domain): 123 | """Creates, saves and returns a record for this domain""" 124 | kwargs = self.get_kwargs(domain) 125 | record, created = Record.objects.update_or_create( 126 | type=kwargs['type'], name=kwargs['name'], 127 | content=kwargs['content'], 128 | defaults=kwargs, 129 | ) 130 | return record 131 | 132 | def update_record(self, record): 133 | kwargs = self.get_kwargs(record.domain) 134 | for kwarg, value in kwargs.items(): 135 | setattr(record, kwarg, value) 136 | 137 | 138 | @receiver( 139 | post_save, sender=Domain, dispatch_uid='domain_update_templated_records' 140 | ) 141 | def update_templated_records(sender, instance, **kwargs): 142 | """Deletes and creates records appropriately to the template""" 143 | if instance.template is None: 144 | return 145 | instance.record_set.exclude( 146 | template__isnull=True 147 | ).exclude( 148 | template__domain_template=instance.template 149 | ).delete() 150 | existing_template_ids = set( 151 | instance.record_set.exclude( 152 | template__isnull=True 153 | ).values_list('template__id', flat=True) 154 | ) 155 | for template in instance.template.recordtemplate_set.exclude( 156 | pk__in=existing_template_ids, 157 | ): 158 | template.create_record(instance) 159 | 160 | 161 | @receiver( 162 | post_save, 163 | sender=RecordTemplate, 164 | dispatch_uid='record_template_modify_templated_records', 165 | ) 166 | def modify_templated_records(sender, instance, created, **kwargs): 167 | if created: 168 | for domain in instance.domain_template.domain_set.all(): 169 | instance.create_record(domain) 170 | else: 171 | for record in instance.record_set.all(): 172 | instance.update_record(record) 173 | record.save() 174 | -------------------------------------------------------------------------------- /powerdns/models/tsigkeys.py: -------------------------------------------------------------------------------- 1 | from django.utils.translation import ugettext_lazy as _ 2 | from django.db import models 3 | 4 | 5 | class TsigKey(models.Model): 6 | ALGORITHME_TYPE = ( 7 | ('hmac-md5', 'hmac-md5'), 8 | ) 9 | name = models.CharField(_("name"), max_length=255, 10 | blank=False, null=False, help_text=_("Key name")) 11 | algorithm = models.CharField(_("algorithm"), max_length=50, blank=False, 12 | null=False, choices=ALGORITHME_TYPE) 13 | secret = models.CharField(_("secret"), max_length=255, blank=False, 14 | null=False, help_text=_("Secret key")) 15 | 16 | class Meta: 17 | db_table = 'tsigkeys' 18 | 19 | def __str__(self): 20 | return self.name 21 | -------------------------------------------------------------------------------- /powerdns/routers.py: -------------------------------------------------------------------------------- 1 | class PowerDNSRouter(object): 2 | """Route all operations on powerdns models to the powerdns database.""" 3 | 4 | db_name = 'powerdns' 5 | app_name = 'powerdns' 6 | 7 | def db_for_read(self, model, **hints): 8 | if model._meta.app_label == self.app_name: 9 | return self.db_name 10 | return None 11 | 12 | def db_for_write(self, model, **hints): 13 | if model._meta.app_label == self.app_name: 14 | return self.db_name 15 | return None 16 | 17 | def allow_relation(self, obj1, obj2, **hints): 18 | if (obj1._meta.app_label == self.app_name and 19 | obj2._meta.app_label == self.app_name): 20 | return True 21 | return None 22 | 23 | def allow_syncdb(self, db, model): 24 | if model._meta.app_label == self.app_name: 25 | return db == self.db_name 26 | elif db == self.db_name: 27 | # workaround for http://south.aeracode.org/ticket/370 28 | return model._meta.app_label == 'south' 29 | return None 30 | -------------------------------------------------------------------------------- /powerdns/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allegro/django-powerdns-dnssec/333bdc668b6cda1a2ff240efd814a1896a1a8e07/powerdns/tests/__init__.py -------------------------------------------------------------------------------- /powerdns/tests/test_events.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | 4 | from powerdns.models import Record 5 | from .utils import RecordTestCase, RecordFactory 6 | 7 | 8 | class TestSOASerialUpdate(RecordTestCase): 9 | 10 | def setUp(self): 11 | super(TestSOASerialUpdate, self).setUp() 12 | self.soa_record = RecordFactory( 13 | domain=self.domain, 14 | type='SOA', 15 | name='example.com', 16 | content=( 17 | 'na1.example.com. hostmaster.example.com. ' 18 | '0 43200 600 1209600 600' 19 | ), 20 | ) 21 | # Less than 1 second will elapse until the test runs, so we update 22 | # this manually while circumventing save() 23 | Record.objects.filter(pk=self.soa_record.pk).update( 24 | change_date=1432720132 25 | ) 26 | self.a_record = RecordFactory( 27 | domain=self.domain, 28 | type='A', 29 | name='www.example.com', 30 | content='192.168.1.1', 31 | ) 32 | self.cname_record = RecordFactory( 33 | domain=self.domain, 34 | type='CNAME', 35 | name='blog.example.com', 36 | content='www.example.com', 37 | ) 38 | 39 | def test_soa_update(self): 40 | """Test if SOA change_date is updated when a record is removed""" 41 | old_serial = Record.objects.get(pk=self.soa_record.pk).change_date 42 | self.a_record.delete() 43 | new_serial = Record.objects.get(pk=self.soa_record.pk).change_date 44 | self.assertGreater(new_serial, old_serial) 45 | -------------------------------------------------------------------------------- /powerdns/tests/test_models.py: -------------------------------------------------------------------------------- 1 | """Tests for record/domain ownership""" 2 | 3 | from django.test import TestCase 4 | 5 | from powerdns.models import OwnershipType 6 | from powerdns.tests.utils import ( 7 | DomainFactory, 8 | DomainOwnerFactory, 9 | ServiceFactory, 10 | ServiceOwnerFactory, 11 | UserFactory, 12 | ) 13 | 14 | 15 | class TestDomain(TestCase): 16 | 17 | def test_domain_return_owners_from_service_and_domain(self): 18 | domain_owner = DomainOwnerFactory() 19 | service_owner = ServiceOwnerFactory() 20 | domain_owner.domain.service = service_owner.service 21 | 22 | self.assertEqual(domain_owner.domain.owners.count(), 2) 23 | domain_users = domain_owner.domain.owners.values_list( 24 | 'username', flat=True, 25 | ) 26 | self.assertTrue( 27 | domain_owner.owner.username in domain_users and 28 | service_owner.owner.username in domain_users 29 | ) 30 | 31 | 32 | class TestServiceOwners(TestCase): 33 | def test_returns_service_owner_when_no_direct_owners(self): 34 | users = UserFactory.create_batch(2) 35 | service = ServiceFactory() 36 | ServiceOwnerFactory( 37 | service=service, 38 | owner=users[0], 39 | ownership_type=OwnershipType.TO.name, 40 | ) 41 | domain = DomainFactory(service=service) 42 | 43 | service_owners = domain.service_owners 44 | 45 | self.assertCountEqual( 46 | service_owners.values_list('owner__username', flat=True), 47 | [users[0].username], 48 | ) 49 | 50 | def test_returns_service_owner_from_correct_domain(self): 51 | users = UserFactory.create_batch(2) 52 | service = ServiceFactory() 53 | ServiceOwnerFactory( 54 | service=service, 55 | owner=users[0], 56 | ownership_type=OwnershipType.TO.name, 57 | ) 58 | ServiceOwnerFactory( 59 | service=ServiceFactory(), 60 | owner=users[1], 61 | ownership_type=OwnershipType.TO.name, 62 | ) 63 | domain = DomainFactory(service=service) 64 | 65 | service_owners = domain.service_owners 66 | 67 | self.assertCountEqual( 68 | service_owners.values_list('owner__username', flat=True), 69 | [users[0].username], 70 | ) 71 | 72 | def test_returns_direct_owners_when_direct_owners_set(self): 73 | users = UserFactory.create_batch(2) 74 | domain = DomainFactory() 75 | DomainOwnerFactory( 76 | domain=domain, 77 | owner=users[0], 78 | ownership_type=OwnershipType.TO.name, 79 | ) 80 | DomainOwnerFactory( 81 | domain=DomainFactory(), 82 | owner=users[1], 83 | ownership_type=OwnershipType.TO.name, 84 | ) 85 | 86 | service_owners = domain.service_owners 87 | 88 | self.assertCountEqual( 89 | service_owners.values_list('owner__username', flat=True), 90 | [users[0].username], 91 | ) 92 | -------------------------------------------------------------------------------- /powerdns/tests/test_permissions.py: -------------------------------------------------------------------------------- 1 | """Tests for permissions""" 2 | 3 | import functools as ft 4 | 5 | from django.contrib.auth import get_user_model 6 | from django.core.urlresolvers import reverse 7 | from django.test import TestCase 8 | 9 | from powerdns.utils import AutoPtrOptions 10 | 11 | from .utils import ( 12 | DomainFactory, 13 | ServiceFactory, 14 | RecordFactory, 15 | user_client, 16 | ) 17 | 18 | 19 | def get_url(model, obj_): 20 | return reverse( 21 | 'api:v2:' + model + '-detail', kwargs={'pk': obj_.pk} 22 | ) 23 | 24 | 25 | get_domain_url = ft.partial(get_url, 'domain') 26 | get_record_url = ft.partial(get_url, 'record') 27 | 28 | 29 | class TestPermissions(TestCase): 30 | """Test class for permission tests""" 31 | 32 | def setUp(self): 33 | self.superuser = get_user_model().objects.create_superuser( 34 | 'superuser', 'superuser@example.com', 'password' 35 | ) 36 | self.user = get_user_model().objects.create_user( 37 | 'user', 'superuser@example.com', 'password' 38 | ) 39 | self.service = ServiceFactory() 40 | self.su_domain = DomainFactory( 41 | name='su.example.com', 42 | owner=self.superuser, 43 | auto_ptr=AutoPtrOptions.NEVER, 44 | ) 45 | self.unrestricted_domain = DomainFactory( 46 | name='unrestricted.example.com', 47 | owner=self.superuser, 48 | unrestricted=True, 49 | auto_ptr=AutoPtrOptions.NEVER, 50 | ) 51 | self.u_domain = DomainFactory( 52 | name='u.example.com', 53 | owner=self.user, 54 | auto_ptr=AutoPtrOptions.NEVER, 55 | ) 56 | self.su_record = RecordFactory( 57 | domain=self.su_domain, 58 | name='www.su.example.com', 59 | type='A', 60 | content='192.168.1.1', 61 | owner=self.superuser, 62 | ) 63 | self.u_record = RecordFactory( 64 | domain=self.u_domain, 65 | name='www.u.example.com', 66 | type='A', 67 | content='192.168.1.2', 68 | owner=self.user, 69 | ) 70 | self.su_client = user_client(self.superuser) 71 | self.u_client = user_client(self.user) 72 | 73 | def test_su_can_edit_all_domains(self): 74 | """Superuser can edit domain not owned by herself.""" 75 | request = self.su_client.patch( 76 | get_domain_url(self.u_domain), 77 | {'type': 'NATIVE'}, 78 | ) 79 | self.assertEqual(request.status_code, 200) 80 | 81 | def test_user_cant_edit_other_domains(self): 82 | """Normal user can't edit domains she doesn't own.""" 83 | request = self.u_client.patch( 84 | get_domain_url(self.su_domain), 85 | {'type': 'NATIVE'}, 86 | ) 87 | self.assertEqual(request.status_code, 403) 88 | 89 | def test_user_cant_edit_her_domains(self): 90 | """Normal user cant edit even owned domains.""" 91 | request = self.u_client.patch( 92 | get_domain_url(self.u_domain), 93 | {'type': 'NATIVE'}, 94 | ) 95 | self.assertEqual(request.status_code, 403) 96 | 97 | def test_user_cant_create_domain(self): 98 | """Normal user can't create domain that is not a child of other domain. 99 | """ 100 | request = self.u_client.post( 101 | reverse('api:v2:domain-list'), 102 | {'name': 'example2.com'} 103 | ) 104 | self.assertEqual(request.status_code, 403) 105 | 106 | def test_user_cant_subdomain_her_own(self): 107 | """Normal user can't create domain that is a child of domain she owns. 108 | """ 109 | request = self.u_client.post( 110 | reverse('api:v2:domain-list'), 111 | {'name': 'subdomain.u.example.com'} 112 | ) 113 | self.assertEqual(request.status_code, 403) 114 | 115 | def test_user_cant_subdomain_others(self): 116 | """Normal user can't create domain that is a child of not owned domain. 117 | """ 118 | request = self.u_client.post( 119 | reverse('api:v2:domain-list'), 120 | {'name': 'subdomain.su.example.com'} 121 | ) 122 | self.assertEqual(request.status_code, 403) 123 | 124 | def test_su_can_edit_all_records(self): 125 | """Superuser can edit record not owned by herself.""" 126 | request = self.su_client.patch( 127 | get_record_url(self.u_record), 128 | {'content': '192.168.1.3'}, 129 | ) 130 | self.assertEqual(request.status_code, 200) 131 | 132 | def test_u_cant_edit_other_records(self): 133 | """Normal user can't edit record not owned by herself, create request 134 | instead.""" 135 | request = self.u_client.patch( 136 | get_record_url(self.su_record), 137 | {'content': '192.168.1.3'}, 138 | ) 139 | self.assertEqual(request.status_code, 202) 140 | 141 | def test_u_can_edit_her_records(self): 142 | """Normal user can edit record not owned by herself.""" 143 | request = self.u_client.patch( 144 | get_record_url(self.u_record), 145 | {'content': '192.168.1.3'}, 146 | ) 147 | self.assertEqual(request.status_code, 200) 148 | 149 | def test_su_can_create_records(self): 150 | """Superuser can create records in domain she doesn't own.""" 151 | request = self.su_client.post( 152 | reverse('api:v2:record-list'), 153 | { 154 | 'name': 'site.u.example.com', 155 | 'content': '192.168.1.4', 156 | 'type': 'A', 157 | 'domain': self.u_domain.id, 158 | 'service': self.service.id, 159 | }, 160 | ) 161 | self.assertEqual(request.status_code, 201) 162 | 163 | def test_user_can_create_records_in_her_domain(self): 164 | """Normal user can create records in domain she owns.""" 165 | request = self.su_client.post( 166 | reverse('api:v2:record-list'), 167 | { 168 | 'name': 'site.u.example.com', 169 | 'content': '192.168.1.4', 170 | 'type': 'A', 171 | 'domain': self.u_domain.id, 172 | 'service': self.service.id, 173 | }, 174 | ) 175 | self.assertEqual(request.status_code, 201) 176 | 177 | def test_user_can_create_records_in_unrestricted_domain(self): 178 | """Normal user can create records in domain that is marked as 179 | 'unrestricted'.""" 180 | request = self.u_client.post( 181 | reverse('api:v2:record-list'), 182 | { 183 | 'name': 'site.u.example.com', 184 | 'content': '192.168.1.4', 185 | 'type': 'A', 186 | 'domain': self.unrestricted_domain.id, 187 | 'service': self.service.id, 188 | }, 189 | ) 190 | self.assertEqual(request.status_code, 201) 191 | 192 | def test_user_cant_create_records_in_other_domains(self): 193 | """Normal user can't create records in domain she doesn't own.""" 194 | request = self.u_client.post( 195 | reverse('api:v2:record-list'), 196 | { 197 | 'name': 'site.u.example.com', 198 | 'content': '192.168.1.4', 199 | 'type': 'A', 200 | 'domain': self.su_domain.id, 201 | 'service': self.service.id, 202 | }, 203 | ) 204 | self.assertEqual(request.status_code, 202) 205 | -------------------------------------------------------------------------------- /powerdns/tests/test_record_validators.py: -------------------------------------------------------------------------------- 1 | """Validation tests""" 2 | 3 | from .utils import RecordTestCase 4 | 5 | 6 | class TestRecordValidators(RecordTestCase): 7 | 8 | def test_valid_a_record(self): 9 | """A record validates when provided with IPv4 address.""" 10 | self.validate( 11 | name='site.example.com', 12 | type='A', 13 | content='192.168.1.1' 14 | ) 15 | 16 | def test_nameless_record(self): 17 | """Record with blank name is invalid""" 18 | self.check_invalid( 19 | name='', 20 | type='A', 21 | content='192.168.1.1' 22 | ) 23 | 24 | def test_invalid_a_record(self): 25 | """A record doesn't validate when provided with IPv6 address.""" 26 | self.check_invalid( 27 | name='site.example.com', 28 | type='A', 29 | content='2001:db8::1428:57ab' 30 | ) 31 | 32 | def test_valid_aaaa_record(self): 33 | """AAAA record validates when provided with IPv6 address.""" 34 | self.validate( 35 | name='site.example.com', 36 | type='AAAA', 37 | content='2001:db8::1428:57ab' 38 | ) 39 | 40 | def test_invalid_aaaa_record(self): 41 | """A record doesn't validate when provided with IPv4 address.""" 42 | self.check_invalid( 43 | name='site.example.com', 44 | type='AAAA', 45 | content='192.168.1.1' 46 | ) 47 | 48 | def test_valid_cname_record(self): 49 | """CNAME record validates with proper domain name""" 50 | self.validate( 51 | name='site.example.com', 52 | type='CNAME', 53 | content='www.example.com' 54 | ) 55 | 56 | def test_valid_record_underscores(self): 57 | """Underscores are allowed in domain name""" 58 | self.validate( 59 | name='something._domainkey.example.com', 60 | type='TXT', 61 | content='v=DKIM1; k=RSA;' 62 | ) 63 | 64 | def test_record_asterisk_name(self): 65 | """CNAME record validates with name beginning in asterisk""" 66 | self.validate( 67 | name='*.site.example.com', 68 | type='CNAME', 69 | content='www.example.com' 70 | ) 71 | 72 | def test_cname_record_trailing_dot(self): 73 | """CNAME record doesn't validate with trailing dot""" 74 | self.check_invalid( 75 | name='site.example.com', 76 | type='CNAME', 77 | content='www.example.com.' 78 | ) 79 | 80 | def test_cname_record_comma(self): 81 | """CNAME record doesn't validate with a comma""" 82 | self.check_invalid( 83 | name='site.example.com', 84 | type='CNAME', 85 | content='www,example.com' 86 | ) 87 | 88 | def test_cname_record_multidot(self): 89 | """CNAME record doesn't validate with a several dots in a row""" 90 | self.check_invalid( 91 | name='site.example.com', 92 | type='CNAME', 93 | content='www..example.com' 94 | ) 95 | 96 | def test_valid_soa_record_dot(self): 97 | """SOA record validates with proper data (trailing dots)""" 98 | 99 | self.validate( 100 | name='site.example.com', 101 | type='SOA', 102 | content=( 103 | 'example.com. hostmaster.example.com. ' 104 | '201503291 43200 600 1209600 600' 105 | ) 106 | ) 107 | 108 | def test_valid_soa_record_nodot(self): 109 | """SOA record validates with proper data (no trailing dots)""" 110 | 111 | self.validate( 112 | name='site.example.com', 113 | type='SOA', 114 | content=( 115 | 'example.com hostmaster.example.com ' 116 | '201503291 43200 600 1209600 600' 117 | ) 118 | ) 119 | 120 | def test_soa_record_missing_fields(self): 121 | """SOA record doesn't validate with missing fields""" 122 | 123 | self.check_invalid( 124 | name='site.example.com', 125 | type='SOA', 126 | content=( 127 | 'example.com. hostmaster.example.com. ' 128 | '201503291 43200 600 1209600' 129 | ) 130 | ) 131 | 132 | def test_soa_record_invalid_domain(self): 133 | """SOA record doesn't validate with invalid domain""" 134 | 135 | self.check_invalid( 136 | name='site.example.com', 137 | type='SOA', 138 | content=( 139 | 'example.com. hostmaster,example.com. ' 140 | '201503291 43200 600 1209600 600' 141 | ) 142 | ) 143 | 144 | def test_soa_record_invalid_time(self): 145 | """SOA record doesn't validate with invalid time field""" 146 | 147 | self.check_invalid( 148 | name='site.example.com', 149 | type='SOA', 150 | content=( 151 | 'example.com. hostmaster.example.com. ' 152 | 'notanumber 43200 600 1209600 600' 153 | ) 154 | ) 155 | -------------------------------------------------------------------------------- /powerdns/tests/test_request_uniqueness_constraints.py: -------------------------------------------------------------------------------- 1 | """Tests for keeping uniqueness constraints for requests""" 2 | 3 | # This file is essentially a 1:1 copy of test_uniqueness_constraints 4 | # The record requests should be validated like records 5 | from django.contrib.auth import get_user_model 6 | 7 | from powerdns.models import Domain, Record, RecordRequest 8 | from .utils import RecordFactory, RecordTestCase 9 | 10 | 11 | class TestRequestUniquenessConstraints(RecordTestCase): 12 | 13 | def setUp(self): 14 | super(TestRequestUniquenessConstraints, self).setUp() 15 | self.a_record = RecordFactory( 16 | domain=self.domain, 17 | type='A', 18 | name='www.example.com', 19 | content='192.168.1.1', 20 | ) 21 | self.cname_record = RecordFactory( 22 | domain=self.domain, 23 | type='CNAME', 24 | name='blog.example.com', 25 | content='www.example.com', 26 | ) 27 | self.user = get_user_model().objects.create_user( 28 | 'user1', 'user1@example.com', 'password' 29 | ) 30 | 31 | def tearDown(self): 32 | for Model in [Domain, Record, get_user_model()]: 33 | Model.objects.all().delete() 34 | 35 | def validate(self, **values): 36 | """ 37 | Perform a full clean of a record with given values""" 38 | values.setdefault('domain', self.domain) 39 | RecordRequest(**values).full_clean() 40 | 41 | def test_nonconflicting_a_record(self): 42 | """The validation allows an A record when it doesn't conflict with 43 | existing CNAME""" 44 | self.validate( 45 | target_type='A', 46 | target_name='wiki.example.com', 47 | target_content='192.168.1.2', 48 | target_owner=self.user, 49 | ) 50 | 51 | def test_noconflict_with_itself(self): 52 | """A CNAME record can be resaved (it doesn't conflict with itself.)""" 53 | self.validate( 54 | record=self.cname_record, 55 | target_type='CNAME', 56 | target_name='blog.example.com', 57 | target_content='www2.example.com', 58 | target_owner=self.user, 59 | ) 60 | 61 | def test_conflicting_a_record(self): 62 | """The validation doesn't allow an A recrod when it conflicts with 63 | existing CNAME""" 64 | self.check_invalid( 65 | target_type='A', 66 | target_name='blog.example.com', 67 | target_content='192.168.1.2', 68 | target_owner=self.user, 69 | ) 70 | 71 | def test_nonconflicting_cname_record(self): 72 | """The validation allows an CNAME record when it doesn't conflict with 73 | existing A""" 74 | self.validate( 75 | target_type='CNAME', 76 | target_name='wiki.example.com', 77 | target_content='site.example.com', 78 | target_owner=self.user, 79 | ) 80 | 81 | def test_conflicting_cname_record(self): 82 | """The validation doesn't allow a CNAME record when it conflicts with 83 | existing A""" 84 | self.check_invalid( 85 | target_type='CNAME', 86 | target_name='www.example.com', 87 | target_content='site.example.com', 88 | target_owner=self.user, 89 | ) 90 | 91 | def test_conflicting_second_cname_record(self): 92 | """The validation doesn't allow a CNAME record when it conflicts with 93 | existing CNAME""" 94 | self.check_invalid( 95 | target_type='CNAME', 96 | target_name='blog.example.com', 97 | target_content='site.example.com', 98 | target_owner=self.user, 99 | ) 100 | -------------------------------------------------------------------------------- /powerdns/tests/test_requests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from threadlocals.threadlocals import set_current_user 4 | from django.contrib.auth import get_user_model 5 | 6 | from powerdns.models import ( 7 | Domain, 8 | DomainRequest, 9 | Record, 10 | RecordRequest, 11 | ) 12 | from .utils import ( 13 | ServiceFactory, 14 | assert_does_exist, 15 | assert_not_exists, 16 | ) 17 | 18 | 19 | class TestRequests(TestCase): 20 | """Tests for domain/record requests""" 21 | 22 | def setUp(self): 23 | self.user1 = get_user_model().objects.create_user( 24 | 'user1', 'user1@example.com', 'password' 25 | ) 26 | self.user2 = get_user_model().objects.create_user( 27 | 'user2', 'user2@example.com', 'password' 28 | ) 29 | self.domain = Domain.objects.create( 30 | name='example.com', 31 | type='NATIVE', 32 | owner=self.user1 33 | ) 34 | self.record = Record.objects.create( 35 | domain=self.domain, 36 | name='forum.example.com', 37 | type='CNAME', 38 | content='phpbb.example.com', 39 | owner=self.user1, 40 | ) 41 | 42 | def test_subdomain_creation(self): 43 | set_current_user(self.user1) 44 | request = DomainRequest.objects.create( 45 | parent_domain=self.domain, 46 | target_name='subdomain.example.com', 47 | target_owner=self.user1, 48 | target_service=ServiceFactory(), 49 | ) 50 | request.accept() 51 | assert_does_exist( 52 | Domain, name='subdomain.example.com', owner=self.user1 53 | ) 54 | 55 | def test_domain_change(self): 56 | request = DomainRequest.objects.create( 57 | domain=self.domain, 58 | target_name='example.com', 59 | target_type='MASTER', 60 | owner=self.user2, 61 | target_owner=self.user1, 62 | target_service=ServiceFactory(), 63 | ) 64 | request.accept() 65 | assert_does_exist( 66 | Domain, 67 | name='example.com', 68 | type='MASTER', 69 | owner=self.user1 70 | ) 71 | assert_not_exists(Domain, name='example.com', type='NATIVE') 72 | 73 | def test_record_creation(self): 74 | request = RecordRequest.objects.create( 75 | domain=self.domain, 76 | target_type='CNAME', 77 | target_name='site.example.com', 78 | target_content='www.example.com', 79 | owner=self.user1, 80 | target_owner=self.user2, 81 | ) 82 | request.accept() 83 | assert_does_exist( 84 | Record, 85 | content='www.example.com', 86 | owner=self.user2, 87 | ) 88 | 89 | def test_record_change(self): 90 | request = RecordRequest.objects.create( 91 | domain=self.domain, 92 | record=self.record, 93 | target_type='CNAME', 94 | target_name='forum.example.com', 95 | target_content='djangobb.example.com', 96 | target_owner=self.user2, 97 | owner=self.user1, 98 | ) 99 | request.accept() 100 | assert_does_exist(Record, content='djangobb.example.com') 101 | assert_not_exists(Record, content='phpbb.example.com') 102 | -------------------------------------------------------------------------------- /powerdns/tests/test_uniqueness_constraints.py: -------------------------------------------------------------------------------- 1 | """Tests for keeping uniqueness constraints""" 2 | 3 | from .utils import RecordFactory, RecordTestCase 4 | 5 | 6 | class TestUniquenessConstraints(RecordTestCase): 7 | 8 | def setUp(self): 9 | super(TestUniquenessConstraints, self).setUp() 10 | self.a_record = RecordFactory( 11 | domain=self.domain, 12 | type='A', 13 | name='www.example.com', 14 | content='192.168.1.1', 15 | ) 16 | self.cname_record = RecordFactory( 17 | domain=self.domain, 18 | type='CNAME', 19 | name='blog.example.com', 20 | content='www.example.com', 21 | ) 22 | 23 | def test_nonconflicting_a_record(self): 24 | """The validation allows an A record when it doesn't conflict with 25 | existing CNAME""" 26 | self.validate(type='A', name='wiki.example.com', content='192.168.1.2') 27 | 28 | def test_noconflict_with_itself(self): 29 | """A CNAME record can be resaved (it doesn't conflict with itself.)""" 30 | self.cname_record.full_clean() 31 | 32 | def test_conflicting_a_record(self): 33 | """The validation doesn't allow an A recrod when it conflicts with 34 | existing CNAME""" 35 | self.check_invalid( 36 | type='A', 37 | name='blog.example.com', 38 | content='192.168.1.2', 39 | ) 40 | 41 | def test_nonconflicting_cname_record(self): 42 | """The validation allows an CNAME record when it doesn't conflict with 43 | existing A""" 44 | self.validate( 45 | type='CNAME', 46 | name='wiki.example.com', 47 | content='site.example.com' 48 | ) 49 | 50 | def test_conflicting_cname_record(self): 51 | """The validation doesn't allow a CNAME record when it conflicts with 52 | existing A""" 53 | self.check_invalid( 54 | type='CNAME', 55 | name='www.example.com', 56 | content='site.example.com' 57 | ) 58 | 59 | def test_conflicting_second_cname_record(self): 60 | """The validation doesn't allow a CNAME record when it conflicts with 61 | existing CNAME""" 62 | self.check_invalid( 63 | type='CNAME', 64 | name='blog.example.com', 65 | content='site.example.com' 66 | ) 67 | -------------------------------------------------------------------------------- /powerdns/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from powerdns.utils import to_reverse, reverse_pointer 4 | 5 | 6 | class TestReversing(TestCase): 7 | @classmethod 8 | def setUpClass(cls): 9 | super().setUpClass() 10 | cls.ipv4 = '192.168.1.2' 11 | cls.ipv6 = '2001:0db8:0:0::1428:57ab' 12 | 13 | def test_reversing_pointer_ipv4(self): 14 | rev = reverse_pointer(self.ipv4) 15 | self.assertEqual(rev, '2.1.168.192.in-addr.arpa') 16 | 17 | def test_reversing_pointer_ipv6(self): 18 | rev = reverse_pointer(self.ipv6) 19 | self.assertEqual( 20 | rev, 'b.a.7.5.8.2.4.1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa' # noqa 21 | ) 22 | 23 | def test_to_reverse_ipv4(self): 24 | last_byte, domain = to_reverse(self.ipv4) 25 | self.assertEqual(domain, '1.168.192.in-addr.arpa') 26 | self.assertEqual(last_byte, '2') 27 | 28 | def test_to_reverse_ipv6(self): 29 | last_byte, domain = to_reverse(self.ipv6) 30 | self.assertEqual(domain, 'a.7.5.8.2.4.1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa') # noqa 31 | self.assertEqual(last_byte, 'b') 32 | -------------------------------------------------------------------------------- /powerdns/tests/utils.py: -------------------------------------------------------------------------------- 1 | """Utilities for tests""" 2 | 3 | import functools as ft 4 | 5 | import factory 6 | from django.contrib.auth import get_user_model 7 | from django.contrib.contenttypes.models import ContentType 8 | from django.core.exceptions import ValidationError 9 | from django.test import TestCase 10 | from factory.django import DjangoModelFactory 11 | from rest_framework.test import APIClient 12 | 13 | from powerdns.models import ( 14 | DeleteRequest, 15 | Domain, 16 | DomainOwner, 17 | DomainTemplate, 18 | Record, 19 | RecordRequest, 20 | RecordTemplate, 21 | Service, 22 | ServiceOwner, 23 | ) 24 | from powerdns.utils import AutoPtrOptions 25 | 26 | 27 | class UserFactory(factory.django.DjangoModelFactory): 28 | class Meta: 29 | model = get_user_model() 30 | django_get_or_create = ('username',) 31 | 32 | username = factory.Sequence(lambda n: "user_%d" % n) 33 | 34 | @classmethod 35 | def _generate(cls, create, attrs): 36 | user = super(UserFactory, cls)._generate(create, attrs) 37 | user.set_password('password') 38 | user.save() 39 | return user 40 | 41 | 42 | class DomainTemplateFactory(DjangoModelFactory): 43 | class Meta: 44 | model = DomainTemplate 45 | 46 | name = factory.Sequence(lambda n: 'name%d' % n) 47 | 48 | 49 | class RecordTemplateFactory(DjangoModelFactory): 50 | class Meta: 51 | model = RecordTemplate 52 | 53 | 54 | class ServiceFactory(DjangoModelFactory): 55 | class Meta: 56 | model = Service 57 | name = factory.Sequence(lambda n: 'service%d' % n) 58 | uid = factory.Sequence(lambda n: 'uid%d' % n) 59 | 60 | 61 | class ServiceOwnerFactory(DjangoModelFactory): 62 | class Meta: 63 | model = ServiceOwner 64 | service = factory.SubFactory(ServiceFactory) 65 | owner = factory.SubFactory(UserFactory) 66 | 67 | 68 | class DomainFactory(DjangoModelFactory): 69 | class Meta: 70 | model = Domain 71 | 72 | name = factory.Sequence(lambda n: 'name%d.com' % n) 73 | service = factory.SubFactory(ServiceFactory) 74 | auto_ptr = AutoPtrOptions.NEVER 75 | 76 | 77 | class DomainOwnerFactory(DjangoModelFactory): 78 | class Meta: 79 | model = DomainOwner 80 | domain = factory.SubFactory(DomainFactory) 81 | owner = factory.SubFactory(UserFactory) 82 | 83 | 84 | class RecordFactory(DjangoModelFactory): 85 | class Meta: 86 | model = Record 87 | 88 | domain = factory.SubFactory(DomainFactory) 89 | owner = factory.SubFactory(UserFactory) 90 | service = factory.SubFactory(ServiceFactory) 91 | 92 | 93 | class RecordRequestFactory(DjangoModelFactory): 94 | class Meta: 95 | model = RecordRequest 96 | 97 | record = factory.SubFactory(RecordFactory) 98 | domain = factory.SubFactory(DomainFactory) 99 | owner = factory.SubFactory(UserFactory) 100 | target_service = factory.SubFactory(ServiceFactory) 101 | 102 | 103 | class RecordDeleteRequestFactory(factory.django.DjangoModelFactory): 104 | class Meta: 105 | model = DeleteRequest 106 | 107 | target = factory.SubFactory(RecordFactory) 108 | owner = factory.SubFactory(UserFactory) 109 | 110 | @property 111 | def content_type(self): 112 | return ContentType.objects.get_for_model(Record) 113 | 114 | 115 | class RecordTestCase(TestCase): 116 | """Base class for tests on records.""" 117 | 118 | def setUp(self): 119 | self.domain = DomainFactory( 120 | name='example.com', 121 | template=None, 122 | reverse_template=DomainTemplateFactory(name='reverse'), 123 | auto_ptr=AutoPtrOptions.NEVER, 124 | ) 125 | 126 | def validate(self, **values): 127 | """ 128 | Perform a full clean of a record with given values""" 129 | values.setdefault('domain', self.domain) 130 | values.setdefault('change_date', '20150305') 131 | Record(**values).full_clean() 132 | 133 | def check_invalid(self, **values): 134 | """ 135 | Perform a full clean of a record with given values expecting it to fail 136 | """ 137 | with self.assertRaises(ValidationError): 138 | self.validate(**values) 139 | 140 | 141 | def user_client(user): 142 | """Returns client for a given user""" 143 | client = APIClient() 144 | client.force_authenticate(user=user) 145 | return client 146 | 147 | 148 | def assert_exists(exists, Model, **kwargs): 149 | """Check if given object exists or doesn't exist""" 150 | if Model.objects.filter(**kwargs).exists() != exists: 151 | raise AssertionError("Object with arguments {} {}!".format( 152 | kwargs, 153 | "doesn't exist" if exists else "exists" 154 | )) 155 | 156 | 157 | assert_does_exist = ft.partial(assert_exists, True) 158 | assert_not_exists = ft.partial(assert_exists, False) 159 | -------------------------------------------------------------------------------- /powerdns/views.py: -------------------------------------------------------------------------------- 1 | """Views and viewsets for DNSaaS API""" 2 | import logging 3 | 4 | from rest_framework.authtoken.models import Token 5 | from rest_framework.authtoken.views import ObtainAuthToken 6 | from rest_framework.response import Response 7 | 8 | 9 | log = logging.getLogger(__name__) 10 | 11 | 12 | class ObtainAuthToken(ObtainAuthToken): 13 | def post(self, request): 14 | serializer = self.serializer_class(data=request.data) 15 | serializer.is_valid(raise_exception=True) 16 | user = serializer.validated_data['user'] 17 | token, created = Token.objects.get_or_create(user=user) 18 | return Response({ 19 | 'user_id': user.id, 20 | 'token': token.key, 21 | 'user': user.get_full_name() or user.username, 22 | 'is_admin': user.is_superuser 23 | }) 24 | 25 | 26 | obtain_auth_token = ObtainAuthToken.as_view() 27 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import json 4 | import os 5 | try: 6 | from setuptools import setup, find_packages 7 | except ImportError: 8 | import ez_setup 9 | ez_setup.use_setuptools() 10 | from setuptools import setup, find_packages 11 | 12 | 13 | with open( 14 | os.path.join(os.path.dirname(__file__), 'README.rst'), 15 | encoding='utf-8' 16 | ) as f: 17 | long_description = f.read() 18 | 19 | with open('version.json') as f: 20 | version = '.'.join(str(part) for part in json.load(f)) 21 | 22 | setup( 23 | name='django-powerdns-dnssec', 24 | version=version, 25 | url='https://github.com/allegro/django-powerdns-dnssec', 26 | license='BSD', 27 | description='PowerDNS administration app for Django', 28 | long_description=long_description, 29 | author='Peter Nixon, Łukasz Langa, pylabs Team', 30 | author_email='pylabs@allegro.pl', 31 | packages=[p for p in find_packages() if not p.startswith('example')], 32 | include_package_data=True, 33 | platforms='any', 34 | classifiers=[ 35 | 'Development Status :: 5 - Production/Stable', 36 | 'Framework :: Django', 37 | 'Intended Audience :: Developers', 38 | 'Intended Audience :: System Administrators', 39 | 'Intended Audience :: Telecommunications Industry', 40 | 'License :: OSI Approved :: BSD License', 41 | 'Natural Language :: English', 42 | 'Operating System :: OS Independent', 43 | 'Programming Language :: Python', 44 | 'Programming Language :: Python :: 3.4', 45 | 'Topic :: Internet :: Name Service (DNS)', 46 | 'Topic :: Internet :: WWW/HTTP', 47 | 'Topic :: Internet :: WWW/HTTP :: WSGI :: Application', 48 | 'Topic :: Software Development :: Libraries :: Python Modules', 49 | ], 50 | install_requires=[ 51 | 'Django>=1.8.13', 52 | 'django-autocomplete-light>=3.2.10', 53 | 'django-extensions>=1.5.5', 54 | 'django-nose>=1.4', 55 | 'dj.choices>=0.10.0', 56 | 'django-threadlocals>=0.8', 57 | ], 58 | extras_require={ 59 | 'docs': [ 60 | 'docutils>=0.12', 61 | ], 62 | 63 | 'dnsaas': [ 64 | 'Django>=1.8.13,<1.9', 65 | 'django-extensions==1.5.5', 66 | # 3.3.3 includes bug, https://github.com/rtfd/readthedocs.org/issues/2101 67 | 'djangorestframework==3.3.2', 68 | 'django-rest-swagger==0.3.8', 69 | 'django-filter==0.15.2', 70 | 'mysqlclient==1.3.7', 71 | 'raven==5.20.0' 72 | ], 73 | 74 | # https://github.com/pypa/pip/issues/1197#issuecomment-228939212 75 | 'tests': [ 76 | 'django-nose>=1.4', 77 | 'nose-cov>=1.6', 78 | 'factory_boy>=2.5.2', 79 | ] 80 | }, 81 | tests_require=[ 82 | 'django-powerdns-dnssec[dnsaas]', 83 | 'django-powerdns-dnssec[tests]', 84 | ], 85 | zip_safe=False, # if only because of the readme file 86 | ) 87 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "system", 5 | "moduleResolution": "node", 6 | "sourceMap": true, 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "removeComments": false, 10 | "noImplicitAny": false, 11 | "outDir": "ui/static/dist/" 12 | }, 13 | "exclude": [ 14 | "node_modules", 15 | "typings/main", 16 | "typings/main.d.ts", 17 | "ui/static/lib" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ambientDependencies": { 3 | "es6-shim": "github:DefinitelyTyped/DefinitelyTyped/es6-shim/es6-shim.d.ts#7de6c3dd94feaeb21f20054b9f30d5dabc5efabd", 4 | "jasmine": "github:DefinitelyTyped/DefinitelyTyped/jasmine/jasmine.d.ts#5c182b9af717f73146399c2485f70f1e2ac0ff2b" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ui/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allegro/django-powerdns-dnssec/333bdc668b6cda1a2ff240efd814a1896a1a8e07/ui/__init__.py -------------------------------------------------------------------------------- /ui/static/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 33 |
34 | 35 |
36 |
37 |
38 | -------------------------------------------------------------------------------- /ui/static/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from "@angular/core"; 2 | import { Location } from "@angular/common"; 3 | import { ROUTER_DIRECTIVES, Route, Router, RouteConfig } from "@angular/router-deprecated"; 4 | import { AuthService } from "./auth/auth.service"; 5 | import { ConfigService } from "./config.service"; 6 | import { DomainComponent } from "./domain/domain.component"; 7 | import { LoginComponent } from "./auth/login.component"; 8 | import { LogoutComponent } from "./auth/logout.component"; 9 | import { RecordComponent } from "./record/record.component"; 10 | import { RecordDetailComponent } from "./record/record-detail.component"; 11 | import { RecordRequestComponent } from "./record-request/record-request.component"; 12 | import { RecordRequestDetailComponent } from "./record-request/record-request-detail.component"; 13 | 14 | 15 | @Component({ 16 | selector: "dnsaas-app", 17 | templateUrl: "static/app/app.component.html", 18 | directives: [ROUTER_DIRECTIVES], 19 | providers: [AuthService], 20 | styles: [" .main { padding-top:45px; }"] 21 | }) 22 | @RouteConfig([ 23 | { path: "/add-record/", name: "AddRecord", component: RecordDetailComponent }, 24 | { path: "/domains", name: "Domains", component: DomainComponent }, 25 | { path: "/login", name: "Login", component: LoginComponent }, 26 | { path: "/logout", name: "Logout", component: LogoutComponent }, 27 | { path: "/records", name: "Records", component: RecordComponent }, 28 | { path: "/record-detail/:id", name: "RecordDetail", component: RecordDetailComponent }, 29 | { path: "/records-requests", name: "RecordRequests", component: RecordRequestComponent }, 30 | { path: "/request-detail/:id", name: "RecordRequestDetail", component: RecordRequestDetailComponent }, 31 | ]) 32 | export class AppComponent implements OnInit { 33 | 34 | homeUrl: string = ConfigService.get("homeUrl"); 35 | 36 | constructor( 37 | public router: Router, 38 | public location: Location, 39 | private authService: AuthService 40 | ) { } 41 | 42 | ngOnInit() { 43 | if (!this.authService.isLoggedIn()) { 44 | this.router.navigate(["Login"]); 45 | } else if (this.location.path().length === 0) { 46 | this.router.navigate(["Records"]); 47 | } 48 | } 49 | 50 | get isLoggedIn(): Boolean { 51 | return this.authService.isLoggedIn(); 52 | } 53 | 54 | get username(): string { 55 | return this.authService.getUsername(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /ui/static/app/app.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | } 4 | #wrap { 5 | min-height: 100%; 6 | } 7 | #main { 8 | overflow:auto; 9 | padding-bottom:60px; /* this needs to be bigger than footer height*/ 10 | } 11 | .footer { 12 | position: relative; 13 | margin-top: -60px; /* negative value of footer height */ 14 | height: 60px; 15 | clear:both; 16 | padding-top:20px; 17 | border-top:1px solid silver; 18 | background-color:#f5f5f5; 19 | } 20 | .footer .powered { 21 | color: #9d9d9d; 22 | } 23 | .glyphicon-spin { 24 | -webkit-animation: spin 1000ms infinite linear; 25 | animation: spin 1000ms infinite linear; 26 | } 27 | @-webkit-keyframes spin { 28 | 0% { 29 | -webkit-transform: rotate(0deg); 30 | transform: rotate(0deg); 31 | } 32 | 100% { 33 | -webkit-transform: rotate(359deg); 34 | transform: rotate(359deg); 35 | } 36 | } 37 | @keyframes spin { 38 | 0% { 39 | -webkit-transform: rotate(0deg); 40 | transform: rotate(0deg); 41 | } 42 | 100% { 43 | -webkit-transform: rotate(359deg); 44 | transform: rotate(359deg); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /ui/static/app/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Inject } from "@angular/core"; 2 | import { Http, Headers, Response } from "@angular/http"; 3 | import { Observable } from "rxjs/Observable"; 4 | import { LocalStorage } from "../local-storage"; 5 | import "rxjs/add/operator/map"; 6 | import "rxjs/add/operator/catch"; 7 | 8 | 9 | @Injectable() 10 | export class AuthService { 11 | 12 | public loggedIn: boolean = false; 13 | 14 | constructor( 15 | private http: Http, 16 | private localStorage: LocalStorage 17 | ) { } 18 | 19 | login(username: string, password: string) { 20 | let headers = new Headers(); 21 | headers.append("Content-Type", "application/json"); 22 | 23 | return this.http 24 | .post( 25 | "/api-token-auth/", 26 | JSON.stringify({"username": username, "password": password }), 27 | { headers } 28 | ) 29 | .map(res => res.json()) 30 | .map((res) => { 31 | if (res.token) { 32 | this.localStorage.set("auth_token", res.token); 33 | this.localStorage.set("auth_username", username); 34 | this.localStorage.set("auth_user_id", res.user_id); 35 | this.localStorage.set("auth_user_fullname", res.user); 36 | this.localStorage.set("is_admin", res.is_admin); 37 | return true; 38 | } 39 | return false; 40 | }).catch(this.handleError); 41 | } 42 | 43 | getToken(): string { 44 | return this.localStorage.get("auth_token"); 45 | } 46 | 47 | isLoggedIn(): boolean { 48 | return (this.localStorage.get("auth_token")) ? true : false; 49 | } 50 | 51 | logout() { 52 | this.localStorage.clear(); 53 | } 54 | 55 | getUsername(): string { 56 | return this.localStorage.get("auth_username"); 57 | } 58 | 59 | getUserId(): number { 60 | return Number(this.localStorage.get("auth_user_id")); 61 | } 62 | 63 | isAdmin(): boolean { 64 | return (this.localStorage.get("is_admin") === "true"); 65 | } 66 | 67 | private handleError(response: any) { 68 | if (response.status === 400 || response.status === 412) { 69 | return Observable.throw(JSON.parse(response._body)); 70 | } 71 | let errMsg = response.message || "Server error"; 72 | console.error(errMsg); 73 | return Observable.throw(errMsg); 74 | } 75 | } 76 | 77 | 78 | export function isLoggedin() { 79 | return localStorage.getItem("auth_token"); 80 | } 81 | -------------------------------------------------------------------------------- /ui/static/app/auth/index.ts: -------------------------------------------------------------------------------- 1 | export { LoginComponent } from "./login.component"; 2 | -------------------------------------------------------------------------------- /ui/static/app/auth/login.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |

DNSaaS

6 |
7 |
8 |
9 | {{ errorMessage }} 10 |
11 |
12 |
13 | 14 | 15 |
16 |
17 | 18 | 19 |
20 | 21 |
22 |
23 |
24 |
25 |
26 | 27 | -------------------------------------------------------------------------------- /ui/static/app/auth/login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from "@angular/core"; 2 | import { NgForm } from "@angular/common"; 3 | import { Router, RouteParams } from "@angular/router-deprecated"; 4 | import { HTTP_PROVIDERS } from "@angular/http"; 5 | import "rxjs/add/observable/throw"; 6 | 7 | import { AuthService } from "./auth.service"; 8 | import { User } from "./user"; 9 | 10 | 11 | @Component({ 12 | styles: ["#login { margin-top:100px; }"], 13 | templateUrl: "/static/app/auth/login.component.html", 14 | providers: [HTTP_PROVIDERS, AuthService], 15 | }) 16 | export class LoginComponent { 17 | 18 | errorMessage: string; 19 | submitted = false; 20 | user: User = new User(); 21 | 22 | constructor( 23 | private router: Router, 24 | private routeParams: RouteParams, 25 | private authService: AuthService 26 | ) { } 27 | 28 | onSubmit() { 29 | this.authService.login( 30 | this.user.username, this.user.password 31 | ).subscribe( 32 | result => { 33 | if (result) { 34 | this.router.navigate(["Records"]); 35 | } 36 | }, 37 | error => { 38 | this.errorMessage = error["non_field_errors"][0]; 39 | } 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ui/static/app/auth/logout.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from "@angular/core"; 2 | import { Router } from "@angular/router-deprecated"; 3 | import { AuthService } from "./auth.service"; 4 | 5 | 6 | @Component({ 7 | template: "", 8 | providers: [AuthService], 9 | }) 10 | export class LogoutComponent implements OnInit { 11 | 12 | constructor( 13 | private router: Router, 14 | private authService: AuthService 15 | ) { } 16 | 17 | ngOnInit() { 18 | this.authService.logout(); 19 | this.router.navigate(["Login"]); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ui/static/app/auth/user.ts: -------------------------------------------------------------------------------- 1 | export class User { 2 | public username: string; 3 | public password: string; 4 | 5 | constructor() { } 6 | } 7 | -------------------------------------------------------------------------------- /ui/static/app/autocomplete/autocomplete.component.html: -------------------------------------------------------------------------------- 1 | 24 | -------------------------------------------------------------------------------- /ui/static/app/autocomplete/autocomplete.component.ts: -------------------------------------------------------------------------------- 1 | import { AfterViewInit, Component, Input } from "@angular/core"; 2 | import { AutocompleteServiceInterface } from "./autocomplete.service"; 3 | 4 | 5 | @Component({ 6 | selector: "autocomplete", 7 | templateUrl: "/static/app/autocomplete/autocomplete.component.html", 8 | styles: [` 9 | a {cursor:pointer;} 10 | .empty-control { padding-top:8px; } 11 | .label { 12 | font-size:13px;padding:5px;padding-left:10px;padding-right:10px;margin-left: -9px; 13 | background-color: rgba(153, 153, 149, 0.22);color:rgba(0, 0, 0, 0.66); 14 | } 15 | .glyphicon { cursor:pointer } 16 | .read-only { padding-top: 10px; } 17 | `] 18 | }) 19 | export class AutocompleteComponent implements AfterViewInit { 20 | @Input("placeholder") placeholder: string; 21 | @Input("service") service: AutocompleteServiceInterface; 22 | @Input("forNgModel") ngModel: any; 23 | @Input("ngModelField") ngModelField: string; 24 | @Input("readOnly") isReadOnly: boolean = false; 25 | @Input("afterSelectAction") afterSelectAction: Function; 26 | @Input("afterRemoveAction") afterRemoveAction: Function; 27 | 28 | activeIndex: number = 0; 29 | currentTextValue: string; 30 | selectValue: number; 31 | _value: string; 32 | showResults: boolean = false; 33 | showInput: boolean = true; 34 | showLabel: boolean = false; 35 | results: Array<{0: number, 1: string}>; 36 | startChar: number = 20; // [space] in ASCII 37 | endChar: number = 126; // ~ in ASCII 38 | 39 | ngAfterViewInit() { 40 | this.selectValue = this.ngModel[this.ngModelField]; 41 | if (this.selectValue) { 42 | this.service.getAutocompleteCurrentValue(this.selectValue).subscribe( 43 | value => { 44 | this._value = value; 45 | this.showInput = false; 46 | this.showLabel = true; 47 | } 48 | ); 49 | } 50 | } 51 | 52 | get value(): string { 53 | return this._value ? this._value : ""; 54 | } 55 | 56 | onKey(event: KeyboardEvent, value: string) { 57 | if (event.keyCode === 27) { 58 | // escape 59 | this.showResults = false; 60 | return; 61 | } else if (event.keyCode === 38) { 62 | // key up 63 | this.prevActiveMatch(); 64 | return; 65 | } else if (event.keyCode === 40) { 66 | // key down 67 | this.nextActiveMatch(); 68 | return; 69 | } 70 | else if (event.keyCode === 13) { 71 | // enter 72 | this.onSelect(this.results[this.activeIndex]); 73 | event.preventDefault(); 74 | return; 75 | } 76 | else if ( 77 | event.keyCode < this.startChar || 78 | event.keyCode > this.endChar && 79 | event.keyCode !== 8 80 | ) 81 | 82 | this.activeIndex = 0; 83 | if (value.length >= 2) { 84 | this.service.getAutocompleteSearchResults(value).subscribe( 85 | response => { 86 | this.results = []; 87 | for (let item in response) { 88 | this.results.push([response[item].id, response[item].name]); 89 | } 90 | if (this.results.length > 0) { 91 | this.showResults = true; 92 | } else { 93 | this.showResults = false; 94 | } 95 | } 96 | ); 97 | } else { 98 | this.showResults = false; 99 | } 100 | } 101 | 102 | prevActiveMatch() { 103 | let index: number = this.activeIndex; 104 | this.activeIndex = index - 1 < 0 ? this.results.length - 1 : index - 1; 105 | } 106 | 107 | nextActiveMatch() { 108 | let index: number = this.activeIndex; 109 | this.activeIndex = index + 1 > this.results.length - 1 ? 0 : index + 1; 110 | } 111 | 112 | isActive(item: {0: number, 1: string}): boolean { 113 | return this.activeIndex === this.results.indexOf(item) ? true : false; 114 | } 115 | 116 | selectActive(item: {0: number, 1: string}) { 117 | this.activeIndex = this.results.indexOf(item); 118 | } 119 | 120 | removeCurrent() { 121 | this.showInput = true; 122 | this.showLabel = false; 123 | this.ngModel[this.ngModelField] = null; 124 | this.currentTextValue = ""; 125 | if (this.afterRemoveAction) { 126 | this.afterRemoveAction(); 127 | } 128 | } 129 | 130 | onSelect(item: {0: number, 1: string}) { 131 | this.showResults = false; 132 | this.selectValue = this.results[this.activeIndex][0]; 133 | this._value = item[1]; 134 | this.ngModel[this.ngModelField] = item[0]; 135 | this.results = []; 136 | this.showInput = false; 137 | this.showLabel = true; 138 | 139 | if (this.afterSelectAction) { 140 | this.afterSelectAction(); 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /ui/static/app/autocomplete/autocomplete.service.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from "rxjs/Observable"; 2 | 3 | 4 | export interface AutocompleteServiceInterface { 5 | getAutocompleteSearchResults(value: string): Observable; 6 | getAutocompleteCurrentValue(id: number): Observable ; 7 | } 8 | -------------------------------------------------------------------------------- /ui/static/app/config.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core"; 2 | 3 | 4 | declare var globalConfig: {[key: string]: any}; 5 | 6 | 7 | @Injectable() 8 | export class ConfigService { 9 | 10 | static get(key: string): any { 11 | return globalConfig[key]; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ui/static/app/directives/highlight.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, ElementRef, Input } from "@angular/core"; 2 | 3 | 4 | @Directive({ 5 | selector: "[highlight]", 6 | host: { 7 | "(mouseenter)": "onMouseEnter()", 8 | "(mouseleave)": "onMouseLeave()" 9 | } 10 | }) 11 | export class HighlightDirective { 12 | 13 | @Input("highlight") highlightColor: string = "#d9edf7"; 14 | 15 | private nativeElement: HTMLElement; 16 | 17 | constructor(el: ElementRef) { 18 | this.nativeElement = el.nativeElement; 19 | } 20 | 21 | private highlight(color: string) { 22 | this.nativeElement.style.backgroundColor = color; 23 | } 24 | 25 | onMouseEnter() { 26 | this.highlight(this.highlightColor); 27 | } 28 | 29 | onMouseLeave() { 30 | this.highlight(null); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ui/static/app/domain/domain.component.html: -------------------------------------------------------------------------------- 1 | 6 |
7 |
8 |
9 |
10 |
11 | 19 |
20 | 21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | {{ errorMessage }} 29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 44 | 47 | 50 | 53 | 54 | 55 |
NameTypeOwnerService
42 | {{ domain.name }} 43 | 45 | {{ domain.type }} 46 | 48 | {{ domain.owner }} 49 | 51 | {{ domain.service_name }} 52 |
56 |
57 | 66 |
67 | -------------------------------------------------------------------------------- /ui/static/app/domain/domain.component.ts: -------------------------------------------------------------------------------- 1 | import { AfterViewInit, Component, OnInit, ViewChild } from "@angular/core"; 2 | import { CanActivate, Router, RouteParams } from "@angular/router-deprecated"; 3 | import { URLSearchParams } from "@angular/http"; 4 | import { DomainService } from "./domain.service"; 5 | import { Domain } from "./domain"; 6 | import { isLoggedin } from "../auth/auth.service"; 7 | import { SearchComponent } from "../search.component"; 8 | import { PaginationComponent } from "../pagination/pagination.component"; 9 | import { HighlightDirective } from "../directives/highlight.directive"; 10 | import "rxjs/add/observable/throw"; 11 | 12 | declare var $: any; 13 | 14 | 15 | @Component({ 16 | templateUrl: "/static/app/domain/domain.component.html", 17 | providers: [DomainService], 18 | directives: [PaginationComponent, HighlightDirective], 19 | styles: [` 20 | h1 small { font-size:14px; } 21 | td { font-size:13px; } 22 | th { font-size:14px; } 23 | `] 24 | }) 25 | @CanActivate(() => isLoggedin()) 26 | export class DomainComponent extends SearchComponent implements OnInit { 27 | 28 | domains: Domain[]; 29 | errorMessage: string; 30 | currentOffset: number = 0; 31 | perPage: number = 100; 32 | totalCount: number; 33 | searchValue: string = ""; 34 | additionalRouteParams: {[key: string]: string} = { 35 | "search": null 36 | }; 37 | @ViewChild("searchInput") searchInput; 38 | 39 | constructor( 40 | private domainService: DomainService, 41 | private router: Router, 42 | private routeParams: RouteParams 43 | ) { 44 | super(); 45 | } 46 | 47 | ngOnInit() { 48 | let url_offset: string = this.routeParams.get("offset"); 49 | this.currentOffset = url_offset ? Number(url_offset) : 0; 50 | let search: string = this.routeParams.get("search"); 51 | this.searchValue = (search !== null) ? search : ""; 52 | this.getDomains(); 53 | } 54 | 55 | ngAfterViewInit() { 56 | $(this.searchInput.nativeElement).focus().get(0).setSelectionRange( 57 | this.searchValue.length, this.searchValue.length 58 | ); 59 | } 60 | 61 | getDomains() { 62 | let params: URLSearchParams = new URLSearchParams(); 63 | params.set("limit", String(this.perPage)); 64 | params.set("offset", String(this.currentOffset)); 65 | this.additionalRouteParams["search"] = this.searchValue; 66 | 67 | if (this.searchValue) { 68 | params.set("search", this.searchValue); 69 | } 70 | 71 | this.domainService.getDomains(params).map( 72 | (response) => response.json() 73 | ).subscribe( 74 | (json) => { 75 | this.totalCount = json.count; 76 | this.domains = json.results; 77 | }, 78 | error => this.errorMessage = error 79 | ); 80 | } 81 | 82 | search(value: string) { 83 | this.currentOffset = 0; 84 | if (value.length > 1) { 85 | this.searchValue = value; 86 | this.getDomains(); 87 | } else { 88 | this.searchValue = null; 89 | this.getDomains(); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /ui/static/app/domain/domain.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core"; 2 | import { URLSearchParams, Response } from "@angular/http"; 3 | import { Observable } from "rxjs/Observable"; 4 | import { HttpClient } from "../http-client"; 5 | import { AutocompleteServiceInterface } from "../autocomplete/autocomplete.service"; 6 | import { Domain } from "./domain"; 7 | import { ConfigService } from "../config.service"; 8 | import "rxjs/add/operator/catch"; 9 | import "rxjs/add/operator/map"; 10 | 11 | 12 | @Injectable() 13 | export class DomainService implements AutocompleteServiceInterface { 14 | 15 | constructor( 16 | private http: HttpClient 17 | ) { } 18 | 19 | getDomains(search: URLSearchParams): Observable { 20 | let url: string = ConfigService.get("domainUrl"); 21 | return this.http.get(url, search).catch(this.handleError); 22 | } 23 | 24 | getDomainById(id: number): Observable { 25 | let url: string = `${ConfigService.get("domainUrl")}${id}/`; 26 | return this.http.get(url).map( 27 | response => { 28 | let json = response.json(); 29 | return json || {}; 30 | } 31 | ).catch(this.handleError); 32 | } 33 | 34 | getAutocompleteSearchResults(value: string): Observable { 35 | let url: string = ConfigService.get("domainUrl"); 36 | let params: URLSearchParams = new URLSearchParams(); 37 | params.set("name", value); 38 | params.set("limit", "10"); 39 | return this.http.get(url, params).map(this.extractData).catch(this.handleError); 40 | } 41 | 42 | getAutocompleteCurrentValue(id: number): Observable { 43 | let url: string = `${ConfigService.get("domainUrl")}${id}/`; 44 | return this.http.get(url).map( 45 | response => { 46 | let json = response.json(); 47 | return json.name || ""; 48 | } 49 | ).catch(this.handleError); 50 | } 51 | 52 | private extractData(res: Response) { 53 | if (res.status < 200 || res.status >= 300) { 54 | throw new Error("Bad response status: " + res.status); 55 | } 56 | let body = res.json(); 57 | return body.results || []; 58 | } 59 | 60 | private handleError(error: any) { 61 | let errMsg = error.message || "Server error"; 62 | console.error(errMsg); 63 | return Observable.throw(errMsg); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /ui/static/app/domain/domain.ts: -------------------------------------------------------------------------------- 1 | export class Domain { 2 | public id: number; 3 | public name: string; 4 | public type: string; 5 | public master: string; 6 | public owner: string; 7 | public service_name: string; 8 | 9 | constructor() { } 10 | } 11 | -------------------------------------------------------------------------------- /ui/static/app/domain/index.ts: -------------------------------------------------------------------------------- 1 | export { DomainComponent } from "./domain.component"; 2 | -------------------------------------------------------------------------------- /ui/static/app/exception-handler.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core"; 2 | import { ConfigService } from "./config.service"; 3 | 4 | 5 | // Raven and ravenEnabled variables from ui/templates/ui/index.html 6 | declare var Raven: any; 7 | 8 | 9 | @Injectable() 10 | export class CustomExceptionHandler { 11 | 12 | call(exception: any, stackTrace?: any, reason?: string): void { 13 | if (ConfigService.get("ravenEnabled")) { 14 | Raven.captureException(exception); 15 | } else { 16 | console.error(exception, stackTrace, reason); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /ui/static/app/flash/flash.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "@angular/core"; 2 | import { FlashService } from "./flash.service"; 3 | 4 | 5 | @Component({ 6 | selector: "flash-message", 7 | template: ` 8 | 14 | `, 15 | }) 16 | export class FlashComponent { 17 | 18 | messages: Array<{0: string, 1: string}> = []; 19 | 20 | constructor(private flashService: FlashService) { 21 | this.flashService.messages$.subscribe( 22 | message => this.messages.push(message) 23 | ); 24 | } 25 | 26 | removeMessage(index: number) { 27 | this.messages.splice(index, 1); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /ui/static/app/flash/flash.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core"; 2 | import { Subject } from "rxjs/Subject"; 3 | 4 | 5 | @Injectable() 6 | export class FlashService { 7 | 8 | private messageSource = new Subject<{0: string, 1: string}>(); 9 | 10 | messages$ = this.messageSource.asObservable(); 11 | 12 | addMessage(message: {0: string, 1: string}) { 13 | this.messageSource.next(message); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /ui/static/app/http-client.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core"; 2 | import { Http, Headers, URLSearchParams, Response } from "@angular/http"; 3 | import { Observable } from "rxjs/Observable"; 4 | import { AuthService } from "./auth/auth.service"; 5 | 6 | 7 | @Injectable() 8 | export class HttpClient { 9 | 10 | apiToken: string; 11 | 12 | constructor( 13 | private http: Http, 14 | private authService: AuthService 15 | ) { } 16 | 17 | getAuthorizationHeader(): Headers { 18 | let headers: Headers = new Headers(); 19 | headers.append("Content-Type", "application/json"); 20 | headers.append("Authorization", `Token ${this.authService.getToken()}`); 21 | headers.append("Accept", "application/json; version=v2"); 22 | return headers; 23 | } 24 | 25 | get(url: string, params?: URLSearchParams): Observable { 26 | return this.http.get(url, { 27 | search: params, 28 | headers: this.getAuthorizationHeader() 29 | }); 30 | } 31 | 32 | delete(url: string): Observable { 33 | return this.http.delete(url, { 34 | headers: this.getAuthorizationHeader() 35 | }); 36 | } 37 | 38 | post(url: string, data: string): Observable { 39 | return this.http.post(url, data, { 40 | headers: this.getAuthorizationHeader() 41 | }); 42 | } 43 | 44 | patch(url: string, data: string): Observable { 45 | return this.http.patch(url, data, { 46 | headers: this.getAuthorizationHeader() 47 | }); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /ui/static/app/index.ts: -------------------------------------------------------------------------------- 1 | export { AppComponent } from "./app.component"; 2 | export { LocalStorage } from "./local-storage"; 3 | -------------------------------------------------------------------------------- /ui/static/app/local-storage.spec.ts: -------------------------------------------------------------------------------- 1 | import { it, describe, expect } from "@angular/core/testing"; 2 | import { LocalStorage } from "./local-storage"; 3 | 4 | 5 | describe("LocalStorageTest", () => { 6 | 7 | let testLocalStorage = new LocalStorage(); 8 | 9 | it("set value should be the same of get", () => { 10 | testLocalStorage.set("key", "value"); 11 | expect(testLocalStorage.get("key")).toBe("value"); 12 | }); 13 | 14 | it("set object value should be the same of get", () => { 15 | let obj = {key: "value"}; 16 | testLocalStorage.setObject("key_object", obj); 17 | expect(testLocalStorage.getObject("key_object").key).toBe(obj.key); 18 | }); 19 | 20 | it("remove key", () => { 21 | testLocalStorage.set("remove_key", "value"); 22 | testLocalStorage.remove("remove_key"); 23 | expect(testLocalStorage.get("remove_key")).toBe(false); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /ui/static/app/local-storage.ts: -------------------------------------------------------------------------------- 1 | import { provide } from "@angular/core"; 2 | 3 | export class LocalStorage { 4 | public localStorage: any; 5 | 6 | constructor() { 7 | if (!localStorage) { 8 | throw new Error("Current browser does not support Local Storage"); 9 | } 10 | this.localStorage = localStorage; 11 | } 12 | 13 | public set(key: string, value: string): void { 14 | this.localStorage[key] = value; 15 | } 16 | 17 | public get(key: string): string { 18 | return this.localStorage[key] || false; 19 | } 20 | 21 | public setObject(key: string, value: any): void { 22 | this.localStorage[key] = JSON.stringify(value); 23 | } 24 | 25 | public getObject(key: string): any { 26 | return JSON.parse(this.localStorage[key] || "{}"); 27 | } 28 | 29 | public remove(key: string): any { 30 | this.localStorage.removeItem(key); 31 | } 32 | 33 | public clear() { 34 | this.localStorage.clear(); 35 | } 36 | } 37 | 38 | export const LOCAL_STORAGE_PROVIDERS: any[] = [ 39 | provide(LocalStorage, { useClass: LocalStorage }) 40 | ]; 41 | -------------------------------------------------------------------------------- /ui/static/app/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode, provide, ExceptionHandler } from "@angular/core"; 2 | import { bootstrap } from "@angular/platform-browser-dynamic"; 3 | import { HTTP_PROVIDERS } from "@angular/http"; 4 | import { ROUTER_PROVIDERS } from "@angular/router-deprecated"; 5 | import { Location, LocationStrategy, HashLocationStrategy } from "@angular/common"; 6 | 7 | import { AppComponent } from "./app.component"; 8 | import { AuthService } from "./auth/auth.service"; 9 | import { ConfigService } from "./config.service"; 10 | import { HttpClient } from "./http-client"; 11 | import { LocalStorage } from "./local-storage"; 12 | import { CustomExceptionHandler } from "./exception-handler"; 13 | 14 | 15 | if (!ConfigService.get("debug")) { 16 | enableProdMode(); 17 | } 18 | 19 | bootstrap(AppComponent, [ 20 | ROUTER_PROVIDERS, 21 | HTTP_PROVIDERS, 22 | provide(ExceptionHandler, {useClass: CustomExceptionHandler}), 23 | provide(LocationStrategy, {useClass: HashLocationStrategy}), 24 | provide(LocalStorage, {useClass: LocalStorage}), 25 | provide(AuthService, {useClass: AuthService}), 26 | provide(HttpClient, {useClass: HttpClient}), 27 | provide(ConfigService, {useClass: ConfigService}), 28 | ]); 29 | -------------------------------------------------------------------------------- /ui/static/app/pagination/pagination.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from "@angular/core"; 2 | import { Router } from "@angular/router-deprecated"; 3 | 4 | 5 | @Component({ 6 | selector: "pagination", 7 | template: ` 8 |
9 | 38 |
39 | `, 40 | styles: ["a { cursor:pointer; }"], 41 | }) 42 | export class PaginationComponent { 43 | @Input() totalCount: number; 44 | @Input() perPage: number; 45 | @Input() currentOffset: number; 46 | @Input() routeName: string; 47 | @Input() additionalRouteParams: {[key: string]: string} = {}; 48 | 49 | allPages: number; 50 | nextOffset: number = 0; 51 | prevOffset: number = 0; 52 | showNext: boolean = false; 53 | showPrev: boolean = false; 54 | 55 | constructor(private router: Router) { } 56 | 57 | get lastPageOffset(): number { 58 | return this.allPages * this.perPage - this.perPage; 59 | } 60 | 61 | get pages(): Array<{0: number, 1: number}> { 62 | let result: Array<{0: number, 1: number}> = []; 63 | let startPage: number = 1; 64 | let totalPage: number = Number(Math.ceil(this.totalCount / this.perPage)); 65 | this.allPages = totalPage; 66 | let paginationLimit: number = 5; 67 | 68 | if (totalPage > paginationLimit) { 69 | let currentPage: number = 1; 70 | if (this.currentOffset !== 0) { 71 | currentPage = Math.ceil(this.currentOffset / this.perPage + 1); 72 | } 73 | startPage = currentPage - Math.floor(paginationLimit / 2); 74 | if (startPage < 1) { 75 | startPage = 1; 76 | } 77 | let endPage: number = startPage + paginationLimit - 1 ; 78 | if (endPage < totalPage) { 79 | totalPage = endPage; 80 | } else { 81 | startPage = totalPage - (paginationLimit - 1); 82 | } 83 | } 84 | 85 | for (let i: number = startPage; i <= totalPage; i++) { 86 | let offset: number = (i - 1) * this.perPage; 87 | result.push([offset, i]); 88 | } 89 | 90 | this.showPrev = true; 91 | this.showNext = true; 92 | this.nextOffset = this.currentOffset + this.perPage; 93 | this.prevOffset = this.currentOffset - this.perPage; 94 | 95 | if (this.prevOffset < 0) { 96 | this.showPrev = false; 97 | } 98 | 99 | if (this.nextOffset >= this.totalCount) { 100 | this.showNext = false; 101 | } 102 | return result; 103 | } 104 | 105 | onSelect(offset: number) { 106 | this.additionalRouteParams["offset"] = String(offset); 107 | this.router.navigate([this.routeName, this.additionalRouteParams]); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /ui/static/app/record-request/index.ts: -------------------------------------------------------------------------------- 1 | export { RecordRequestComponent } from "./record-request.component"; 2 | -------------------------------------------------------------------------------- /ui/static/app/record-request/record-request-detail.component.html: -------------------------------------------------------------------------------- 1 | 8 |
9 | Your change requires acceptance by owner of record or domain. 10 |
11 |
12 |
13 |
14 |
15 |
16 | {{ errorMessage }} 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 57 | 58 | 59 |
Name
Type
Content
Priority
TTL
Remarks
Created{{ recordRequest.created }}
Domain{{ domain.name }}
Record 55 | {{ record.name }} 56 |
60 |
61 | 62 |
63 | -------------------------------------------------------------------------------- /ui/static/app/record-request/record-request-detail.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from "@angular/core"; 2 | import { ROUTER_DIRECTIVES, CanActivate, RouteParams, Router } from "@angular/router-deprecated"; 3 | import { isLoggedin } from "../auth/auth.service"; 4 | import { Domain } from "../domain/domain"; 5 | import { DomainService } from "../domain/domain.service"; 6 | import { Record } from "../record/record"; 7 | import { RecordService } from "../record/record.service"; 8 | import { RecordRequestService } from "./record-request.service"; 9 | import { RecordRequest } from "./record-request"; 10 | 11 | 12 | declare var $: any; 13 | 14 | 15 | @Component({ 16 | templateUrl: "/static/app/record-request/record-request-detail.component.html", 17 | providers: [DomainService, RecordService, RecordRequestService], 18 | directives: [ROUTER_DIRECTIVES], 19 | styles: [` 20 | td span { cursor:pointer; } 21 | :host >>> .new { color: green; } 22 | :host >>> .old { color: silver; } 23 | `] 24 | }) 25 | @CanActivate(() => isLoggedin()) 26 | export class RecordRequestDetailComponent implements OnInit { 27 | 28 | domain: Domain; 29 | record: Record; 30 | recordRequest: RecordRequest; 31 | showAutoAcceptanceMessage: boolean = false; 32 | backUrlParams: {[key: string]: string} = {}; 33 | 34 | constructor( 35 | private routeParams: RouteParams, 36 | private router: Router, 37 | private recordRequestService: RecordRequestService, 38 | private domainService: DomainService, 39 | private recordService: RecordService 40 | ) { } 41 | 42 | getValue(fieldName: string): string { 43 | if ($.isEmptyObject(this.recordRequest.last_change)) { 44 | let value: string = this.recordRequest[`target_${fieldName}`]; 45 | return `${value}`; 46 | } 47 | 48 | let newValue: string = String( 49 | this.recordRequest.last_change[fieldName]["new"] 50 | ); 51 | let oldValue: string = String( 52 | this.recordRequest.last_change[fieldName]["old"] 53 | ); 54 | 55 | if (oldValue === newValue) { 56 | return `${newValue}`; 57 | } else { 58 | let result: string = ""; 59 | if (this.recordRequest.last_change["_request_type"] === "update") { 60 | result += `${oldValue} ->`; 61 | } 62 | return result + `${newValue}`; 63 | } 64 | } 65 | 66 | getDomain() { 67 | this.domainService.getDomainById( 68 | this.recordRequest.domain 69 | ).subscribe( 70 | (domain) => this.domain = domain 71 | ); 72 | } 73 | 74 | getRecord() { 75 | if (this.recordRequest.record) { 76 | this.recordService.getRecordById( 77 | String(this.recordRequest.record) 78 | ).subscribe( 79 | (record) => this.record = record 80 | ); 81 | } 82 | } 83 | 84 | ngOnInit() { 85 | let backUrl: string = this.routeParams.get("backUrl"); 86 | if (backUrl && backUrl.length > 0) { 87 | this.backUrlParams = JSON.parse(backUrl); 88 | } 89 | 90 | let requestId: any = this.routeParams.get("id"); 91 | let showAutoAcceptanceMessage: string = this.routeParams.get("showAutoAcceptanceMessage"); 92 | if (requestId) { 93 | this.recordRequestService.getRequestById( 94 | String(requestId) 95 | ).subscribe( 96 | (recordRequest) => { 97 | this.recordRequest = recordRequest; 98 | this.getDomain(); 99 | this.getRecord(); 100 | if (showAutoAcceptanceMessage === "true") { 101 | this.showAutoAcceptanceMessage = true; 102 | } 103 | } 104 | ); 105 | } 106 | } 107 | 108 | onSelectRecord(record: Record) { 109 | this.router.navigate(["RecordDetail", { id: record.id }]); 110 | } 111 | 112 | onBack() { 113 | this.router.navigate(["RecordRequests", this.backUrlParams]); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /ui/static/app/record-request/record-request.component.html: -------------------------------------------------------------------------------- 1 | 4 |
5 |
6 |
7 | 8 | 9 | 10 |
11 |
12 |
13 |
14 | {{ errorMessage }} 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 36 | 39 | 44 | 47 | 50 | 53 | 56 | 59 | 60 | 61 |
NameTypeContentTTLOwnerCreatedTicketOptions
32 |
33 | {{ request.target_name }} 34 |
35 |
37 | {{ request.target_type }} 38 | 40 |
41 | {{ request.target_content }} 42 |
43 |
45 | {{ request.target_ttl }} 46 | 48 | {{ request.target_owner }} 49 | 51 | {{ request.created }} 52 | 54 | {{ request.key }} 55 | 57 | Detail 58 |
62 |
63 | 65 |
66 | -------------------------------------------------------------------------------- /ui/static/app/record-request/record-request.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from "@angular/core"; 2 | import { URLSearchParams } from "@angular/http"; 3 | import { CanActivate, Router, RouteParams } from "@angular/router-deprecated"; 4 | import { AuthService, isLoggedin } from "../auth/auth.service"; 5 | import { RecordRequest } from "./record-request"; 6 | import { RecordRequestService } from "./record-request.service"; 7 | import { HighlightDirective } from "../directives/highlight.directive"; 8 | import { ConfigService } from "../config.service"; 9 | 10 | @Component({ 11 | templateUrl: "/static/app/record-request/record-request.component.html", 12 | providers: [RecordRequestService], 13 | directives: [HighlightDirective], 14 | styles: [` 15 | .panel-heading {overflow:hidden;} 16 | .wrap { text-overflow:ellipsis;overflow:hidden;white-space:nowrap;max-width:200px; } 17 | td { font-size:13px; } 18 | th { font-size:14px; } 19 | `] 20 | }) 21 | @CanActivate(() => isLoggedin()) 22 | export class RecordRequestComponent implements OnInit { 23 | 24 | recordRequests: RecordRequest; 25 | state: string = "pending"; 26 | errorMessage: string; 27 | jiraUrl: string = ConfigService.get("jiraUrl"); 28 | 29 | constructor( 30 | private router: Router, 31 | private recordRequestService: RecordRequestService, 32 | private routeParams: RouteParams 33 | ) { } 34 | 35 | ngOnInit() { 36 | let state: string = this.routeParams.get("state"); 37 | this.state = (state) ? state : "pending"; 38 | this.getRecordRequest(); 39 | } 40 | 41 | onSelect(request: RecordRequest) { 42 | this.router.navigate( 43 | ["RecordRequestDetail", { 44 | id: request.id, 45 | backUrl: JSON.stringify(this.routeParams.params) 46 | }] 47 | ); 48 | } 49 | 50 | getRecordRequest() { 51 | let search: URLSearchParams = new URLSearchParams(); 52 | if (this.state === "accepted") { 53 | search.set("state", "2"); 54 | } else if (this.state === "rejected") { 55 | search.set("state", "3"); 56 | } else { 57 | search.set("state", "1"); 58 | } 59 | this.recordRequestService.getRequests(search).map( 60 | (response) => response.json() 61 | ).subscribe( 62 | (json) => this.recordRequests = json.results 63 | ); 64 | } 65 | 66 | onSelectShowRequest(show: string) { 67 | this.state = show; 68 | this.router.navigate(["RecordRequests", { state: this.state }]); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /ui/static/app/record-request/record-request.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core"; 2 | import { URLSearchParams, Response } from "@angular/http"; 3 | import { Observable } from "rxjs/Observable"; 4 | import { ConfigService } from "../config.service"; 5 | import { RecordRequest } from "./record-request"; 6 | import { HttpClient } from "../http-client"; 7 | import { AuthService } from "../auth/auth.service"; 8 | import "rxjs/add/operator/catch"; 9 | import "rxjs/add/operator/map"; 10 | 11 | 12 | @Injectable() 13 | export class RecordRequestService { 14 | 15 | constructor( 16 | private http: HttpClient, 17 | private authService: AuthService 18 | ) { } 19 | 20 | getRequests(search: URLSearchParams): Observable { 21 | search.set("owner", String(this.authService.getUserId())); 22 | let url: string = ConfigService.get("recordRequestUrl"); 23 | return this.http.get(url, search).catch(this.handleError); 24 | } 25 | 26 | getRequestById(id: string): Observable { 27 | let url: string = `${ConfigService.get("recordRequestUrl")}${id}/`; 28 | return this.http.get(url).map( 29 | this.extractSingleData 30 | ).catch(this.handleError); 31 | } 32 | 33 | private extractSingleData(res: Response) { 34 | if (res.status < 200 || res.status >= 300) { 35 | throw new Error("Bad response status: " + res.status); 36 | } 37 | let body = res.json(); 38 | return body || {}; 39 | } 40 | 41 | private handleError(response: any) { 42 | let errMsg = response.message || "Server error"; 43 | console.error(errMsg); 44 | return Observable.throw(errMsg); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /ui/static/app/record-request/record-request.ts: -------------------------------------------------------------------------------- 1 | 2 | export class RecordRequest { 3 | id: number; 4 | owner: string; 5 | target_name: string; 6 | target_type: string; 7 | target_content: string; 8 | target_prio: string; 9 | target_ttl: string; 10 | target_auth: string; 11 | target_remarks: string; 12 | created: string; 13 | domain: number; 14 | record: number; 15 | last_change: JSON; 16 | key: string; 17 | } 18 | -------------------------------------------------------------------------------- /ui/static/app/record/index.ts: -------------------------------------------------------------------------------- 1 | export { RecordComponent } from "./record.component"; 2 | -------------------------------------------------------------------------------- /ui/static/app/record/record.component.html: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 |
7 |
8 |
9 |
10 | 18 |
19 | 20 |
21 |
22 |
23 |
24 |
25 | - Your records 26 |
27 |
28 |
29 |
30 | 31 | 32 |
33 |
34 |
35 |
36 |
37 |
38 | {{ errorMessage }} 39 |
40 |
41 | You do not own any records, switch to "all records" to display a full list 42 |
43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 62 | 67 | 70 | 75 | 78 | 81 | 84 | 87 | 109 | 110 | 111 |
NameTypeContentTTLOwnerServiceMod. dateAction
63 |
64 | {{ record.name }} 65 |
66 |
68 | {{ record.type }} 69 | 71 |
72 | {{ record.content }} 73 |
74 |
76 | {{ record.ttl }} 77 | 79 | {{ record.owner }} 80 | 82 | {{ record.service_name || '-' }} 83 | 85 | {{ record.modified }} 86 | 88 | 89 | 94 | {{ record.delete_request ? record.delete_request : record.change_request }} 95 | 96 | 97 | 98 |
99 |
100 | Edit 101 | Delete 102 |
103 |
104 | Request Edit 105 | Request Delete 106 |
107 |
108 |
112 |
113 | 122 |
123 | -------------------------------------------------------------------------------- /ui/static/app/record/record.component.ts: -------------------------------------------------------------------------------- 1 | import { AfterViewInit, Component, OnInit, ViewChild } from "@angular/core"; 2 | import { CanActivate, Router, RouteParams } from "@angular/router-deprecated"; 3 | import { URLSearchParams, HTTP_PROVIDERS } from "@angular/http"; 4 | import { AuthService, isLoggedin } from "../auth/auth.service"; 5 | import { ConfigService } from "../config.service"; 6 | import { Record } from "./record"; 7 | import { RecordService } from "./record.service"; 8 | import { PaginationComponent } from "../pagination/pagination.component"; 9 | import { SearchComponent } from "../search.component"; 10 | import { HighlightDirective } from "../directives/highlight.directive"; 11 | import { TooltipDirective } from "../tooltip.directive"; 12 | import { FlashService } from "../flash/flash.service"; 13 | import { FlashComponent } from "../flash/flash.component"; 14 | import "rxjs/add/observable/throw"; 15 | 16 | declare var $: any; 17 | 18 | @Component({ 19 | templateUrl: "/static/app/record/record.component.html", 20 | providers: [HTTP_PROVIDERS, RecordService, FlashService], 21 | directives: [ 22 | FlashComponent, PaginationComponent, HighlightDirective, TooltipDirective 23 | ], 24 | styles: [` 25 | .panel-heading {overflow:hidden;} td { font-size:13px; } 26 | .legend { float:left;padding-top:5px; } 27 | .legend span { 28 | background-color: #dff0d8;width:20px;height:20px; 29 | border: 1px solid #ddd;margin-right:5px; 30 | } 31 | .read-only td { color:silver; cursor: not-allowed; } 32 | .wrap { text-overflow:ellipsis;overflow:hidden;white-space:nowrap;max-width:200px; } 33 | .action { max-width:150px; } 34 | .action .btn { margin-top:2px; } 35 | .ttl { max-width:60px; } 36 | .type { max-width:60px; } 37 | th { font-size:14px; } 38 | `] 39 | }) 40 | @CanActivate(() => isLoggedin()) 41 | export class RecordComponent extends SearchComponent implements AfterViewInit, OnInit { 42 | 43 | records: Record[]; 44 | errorMessage: any; 45 | currentOffset: number = 0; 46 | perPage: number = 100; 47 | totalCount: number; 48 | showAllRecords: boolean = false; 49 | activeUser: string; 50 | searchValue: string = ""; 51 | additionalRouteParams: {[key: string]: string} = { 52 | "showAll": "false", 53 | "search": "" 54 | }; 55 | showResults: boolean = false; 56 | isAdmin: boolean = false; 57 | jiraUrl: string = ConfigService.get("jiraUrl"); 58 | @ViewChild("searchInput") searchInput; 59 | 60 | constructor( 61 | private router: Router, 62 | private routeParams: RouteParams, 63 | private recordService: RecordService, 64 | private authService: AuthService, 65 | private flashService: FlashService 66 | ) { 67 | super(); 68 | } 69 | 70 | ngOnInit() { 71 | this.activeUser = this.authService.getUsername(); 72 | this.isAdmin = this.authService.isAdmin(); 73 | this.showAllRecords = this.routeParams.get("showAll") === "true" ? true : false; 74 | this.additionalRouteParams["showAll"] = this.routeParams.get("showAll"); 75 | let url_offset: string = this.routeParams.get("offset"); 76 | this.currentOffset = url_offset ? Number(url_offset) : 0; 77 | let search: string = this.routeParams.get("search"); 78 | this.searchValue = (search !== null) ? search : ""; 79 | 80 | if (this.routeParams.get("showSaveRecordMessage") === "true") { 81 | this.flashService.addMessage(["success", "Record has been saved."]); 82 | } else if (this.routeParams.get("showAddRecordMessage") === "true") { 83 | this.flashService.addMessage(["success", "Record has been added."]); 84 | } 85 | 86 | this.getRecords(); 87 | } 88 | 89 | ngAfterViewInit() { 90 | $(this.searchInput.nativeElement).focus().get(0).setSelectionRange( 91 | this.searchValue.length, this.searchValue.length 92 | ); 93 | } 94 | 95 | search(value: string) { 96 | if (value.length > 1) { 97 | this.searchValue = value; 98 | } else { 99 | this.searchValue = ""; 100 | } 101 | this.searchUpdateUrls(); 102 | } 103 | 104 | searchUpdateUrls() { 105 | this.additionalRouteParams["search"] = this.searchValue; 106 | this.router.navigate(["Records", this.additionalRouteParams]); 107 | } 108 | 109 | get isRecords(): boolean { 110 | if (typeof this.records === "object") { 111 | return this.records.length === 0 ? false : true; 112 | } 113 | return false; 114 | } 115 | 116 | getRecords() { 117 | this.showResults = false; 118 | let params: URLSearchParams = new URLSearchParams(); 119 | params.set("limit", String(this.perPage)); 120 | params.set("offset", String(this.currentOffset)); 121 | 122 | if (!this.showAllRecords) { 123 | params.set("owner", String(this.authService.getUserId())); 124 | } 125 | this.additionalRouteParams["search"] = this.searchValue; 126 | 127 | if (this.searchValue) { 128 | params.set("search", this.searchValue); 129 | } 130 | 131 | this.recordService.getRecords(params).map( 132 | (response) => response.json() 133 | ).subscribe((json) => { 134 | this.totalCount = json.count; 135 | this.records = json.results; 136 | this.showResults = true; 137 | }, error => this.errorMessage = error); 138 | } 139 | 140 | onSelectShowAllRecords(show?: string) { 141 | if (show === "all") { 142 | this.additionalRouteParams["showAll"] = "true"; 143 | } else { 144 | this.additionalRouteParams["showAll"] = "false"; 145 | } 146 | this.router.navigate(["Records", this.additionalRouteParams]); 147 | } 148 | 149 | onSelect(record: Record) { 150 | if (record.type !== "PTR") { 151 | this.router.navigate( 152 | ["RecordDetail", { id: record.id, backUrl: JSON.stringify(this.routeParams.params) }] 153 | ); 154 | } 155 | } 156 | 157 | deleteConfirm(record: Record) { 158 | if (confirm("Are you sure to delete this record: " + record.content)) { 159 | this.recordService.deleteRecord(record).subscribe((response) => { 160 | if (response.status === 204) { 161 | this.flashService.addMessage(["success", `Record ${record.content } has been successfully removed.`]); 162 | this.getRecords(); 163 | } 164 | }); 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /ui/static/app/record/record.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core"; 2 | import { URLSearchParams, Response } from "@angular/http"; 3 | import { Observable } from "rxjs/Observable"; 4 | import { ConfigService } from "../config.service"; 5 | import { Record } from "./record"; 6 | import { HttpClient } from "../http-client"; 7 | 8 | import "rxjs/add/operator/catch"; 9 | import "rxjs/add/operator/map"; 10 | 11 | 12 | @Injectable() 13 | export class RecordService { 14 | 15 | constructor( 16 | private http: HttpClient 17 | ) { } 18 | 19 | getRecords(search: URLSearchParams): Observable { 20 | let url: string = ConfigService.get("recordUrl"); 21 | return this.http.get(url, search).catch(this.handleError); 22 | } 23 | 24 | getRecordById(id: string): Observable { 25 | let url: string = `${ConfigService.get("recordUrl")}${id}/`; 26 | return this.http.get(url).map( 27 | this.extractSingleData 28 | ).catch(this.handleError); 29 | } 30 | 31 | updateOrCreateRecord(record: Record): Observable { 32 | let url: string = ConfigService.get("recordUrl"); 33 | let body = JSON.stringify(record); 34 | let request: any; 35 | if (record.id) { 36 | url = `${ConfigService.get("recordUrl")}${record.id}/`; 37 | request = this.http.patch(url, body); 38 | } else { 39 | request = this.http.post(url, body); 40 | } 41 | 42 | return request.map( 43 | this.extractSingleData 44 | ).catch(this.handleError); 45 | } 46 | 47 | deleteRecord(record: Record) { 48 | let url: string = `${ConfigService.get("recordUrl")}${record.id}/`; 49 | return this.http.delete(url).catch(this.handleError); 50 | } 51 | 52 | private extractSingleData(res: Response) { 53 | if (res.status < 200 || res.status >= 300) { 54 | throw new Error("Bad response status: " + res.status); 55 | } 56 | let body = res.json(); 57 | return body || {}; 58 | } 59 | 60 | private handleError(response: any) { 61 | if (response.status === 400 || response.status === 412) { 62 | return Observable.throw(JSON.parse(response._body)); 63 | } 64 | let errMsg = response.message || "Server error"; 65 | console.error(errMsg); 66 | return Observable.throw(errMsg); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /ui/static/app/record/record.ts: -------------------------------------------------------------------------------- 1 | import { Domain } from "../domain/domain"; 2 | 3 | 4 | export class Record { 5 | public id: number; 6 | public domain: number; 7 | public service: number; 8 | public name: string; 9 | public content: string; 10 | public type: string; 11 | public remarks: string; 12 | public prio: number; 13 | public ttl: number = 3600; 14 | public owner: string; 15 | public modified: string; 16 | public record_request_id: number; 17 | public change_request: string; 18 | public delete_request: string; 19 | public unrestricted_domain: boolean; 20 | 21 | public static recordTypes: Array<{0: string, 1: string}> = [ 22 | ["A", "IPv4 address: e.g. example.com -> 1.2.3.4"], 23 | ["AAAA", "IPv6 address: e.g. example.com -> 2001:0db8:85a3:0000:0000:8a2e:0370:7334"], 24 | ["CNAME", "alias: e.g. www.example.com -> example.com"], 25 | ["MX", "mail exchange: e.g. mx.example.com -> 1.2.3.4 "], 26 | ["TXT", "text: e.g. example.com -> \"description here\""], 27 | ["SRV", "service locator: e.g. _xmpp-client._tcp.example.com -> 0 5222 jabber.example.com"] 28 | ]; 29 | 30 | constructor() { } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /ui/static/app/search.component.ts: -------------------------------------------------------------------------------- 1 | 2 | export abstract class SearchComponent { 3 | 4 | timer: any; 5 | 6 | abstract search(value: string): void; 7 | 8 | onKeyUpSearch(event: KeyboardEvent, value: string) { 9 | if (this.timer) { 10 | clearTimeout(this.timer); 11 | } 12 | this.timer = setTimeout(() => this.search(value), 500); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /ui/static/app/service/service.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core"; 2 | import { URLSearchParams, Response } from "@angular/http"; 3 | import { Observable } from "rxjs/Observable"; 4 | import { HttpClient } from "../http-client"; 5 | import { AutocompleteServiceInterface } from "../autocomplete/autocomplete.service"; 6 | import { Service } from "./service"; 7 | import { ConfigService } from "../config.service"; 8 | import "rxjs/add/operator/catch"; 9 | import "rxjs/add/operator/map"; 10 | 11 | 12 | @Injectable() 13 | export class ServiceService implements AutocompleteServiceInterface { 14 | 15 | constructor( 16 | private http: HttpClient 17 | ) { } 18 | 19 | getServices(search: URLSearchParams): Observable { 20 | let url: string = ConfigService.get("serviceUrl"); 21 | return this.http.get(url, search).catch(this.handleError); 22 | } 23 | 24 | getServiceById(id: number): Observable { 25 | let url: string = `${ConfigService.get("serviceUrl")}${id}/`; 26 | return this.http.get(url).map( 27 | response => { 28 | let json = response.json(); 29 | return json || {}; 30 | } 31 | ).catch(this.handleError); 32 | } 33 | 34 | getAutocompleteSearchResults(value: string): Observable { 35 | let url: string = ConfigService.get("serviceUrl"); 36 | let params: URLSearchParams = new URLSearchParams(); 37 | params.set("search", value); 38 | params.set("limit", "10"); 39 | return this.http.get(url, params).map(this.extractData).catch(this.handleError); 40 | } 41 | 42 | getAutocompleteCurrentValue(id: number): Observable { 43 | let url: string = `${ConfigService.get("serviceUrl")}${id}/`; 44 | return this.http.get(url).map( 45 | response => { 46 | let json = response.json(); 47 | return json.name || ""; 48 | } 49 | ).catch(this.handleError); 50 | } 51 | 52 | private extractData(res: Response) { 53 | if (res.status < 200 || res.status >= 300) { 54 | throw new Error("Bad response status: " + res.status); 55 | } 56 | let body = res.json(); 57 | return body.results || []; 58 | } 59 | 60 | private handleError(error: any) { 61 | let errMsg = error.message || "Server error"; 62 | console.error(errMsg); 63 | return Observable.throw(errMsg); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /ui/static/app/service/service.ts: -------------------------------------------------------------------------------- 1 | export class Service { 2 | public id: number; 3 | public name: string; 4 | 5 | constructor() { } 6 | } 7 | -------------------------------------------------------------------------------- /ui/static/app/tooltip.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, ElementRef, OnInit } from "@angular/core"; 2 | 3 | declare var $: any; 4 | 5 | 6 | @Directive({ 7 | selector: "[tooltip]" 8 | }) 9 | export class TooltipDirective implements OnInit { 10 | 11 | constructor(private el: ElementRef) { } 12 | 13 | public ngOnInit() { 14 | $(this.el.nativeElement).tooltip(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /ui/static/pylabs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allegro/django-powerdns-dnssec/333bdc668b6cda1a2ff240efd814a1896a1a8e07/ui/static/pylabs.png -------------------------------------------------------------------------------- /ui/static/systemjs.config.js: -------------------------------------------------------------------------------- 1 | (function (global) { 2 | var map = { 3 | 'rxjs': '/static/lib/rxjs', 4 | }; 5 | 6 | //packages tells the System loader how to load when no filename and/or no extension 7 | var packages = { 8 | 'rxjs': {defaultExtension: 'js'}, 9 | 'angular2-in-memory-web-api': {defaultExtension: 'js'}, 10 | '/static/dist': {defaultExtension: 'js'}, 11 | }; 12 | 13 | var PackageNames = [ 14 | 'common', 15 | 'compiler', 16 | 'core', 17 | 'http', 18 | 'platform-browser', 19 | 'platform-browser-dynamic', 20 | 'router-deprecated', 21 | 'upgrade', 22 | ]; 23 | 24 | // Add map entries for each angular package 25 | PackageNames.forEach(function (pkgName) { 26 | map['@angular/' + pkgName] = '/static/lib/@angular/' + pkgName; 27 | }); 28 | 29 | // Add package entries for angular packages 30 | PackageNames.forEach(function (pkgName) { 31 | packages['@angular/' + pkgName] = {main: pkgName + '.umd.js', defaultExtension: 'js'}; 32 | }); 33 | 34 | var config = { 35 | map: map, 36 | packages: packages, 37 | paths: { 38 | 'rxjs/*' : '/static/lib/rxjs/*.js', 39 | } 40 | }; 41 | if (global.filterSystemConfig) { 42 | global.filterSystemConfig(config); 43 | } 44 | System.config(config); 45 | 46 | 47 | })(this); 48 | /* 49 | Copyright 2016 Google Inc. All Rights Reserved. 50 | Use of this source code is governed by an MIT-style license that 51 | can be found in the LICENSE file at http://angular.io/license 52 | */ 53 | -------------------------------------------------------------------------------- /ui/static/unit-tests.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | DNSaaS App Unit Tests 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 68 | 69 | -------------------------------------------------------------------------------- /ui/templates/ui/index.html: -------------------------------------------------------------------------------- 1 | {% load staticfiles %} 2 | 3 | 4 | DNSaaS 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 33 | 34 | 35 | 36 | Loading... 37 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /ui/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.conf import settings 3 | from django.shortcuts import render_to_response 4 | from django.template.context import RequestContext 5 | 6 | 7 | def start_page(request): 8 | return render_to_response( 9 | 'ui/index.html', {'settings': settings}, 10 | context_instance=RequestContext(request), 11 | ) 12 | -------------------------------------------------------------------------------- /version.json: -------------------------------------------------------------------------------- 1 | [2, 6, 29] 2 | --------------------------------------------------------------------------------