diff --git a/designate/api/v2/controllers/blacklists.py b/designate/api/v2/controllers/blacklists.py new file mode 100644 index 000000000..bdfc64bff --- /dev/null +++ b/designate/api/v2/controllers/blacklists.py @@ -0,0 +1,134 @@ +# Copyright 2014 Rackspace +# +# Author: Betsy Luzader +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import pecan +from designate.central import rpcapi as central_rpcapi +from designate.openstack.common import log as logging +from designate import schema +from designate import utils +from designate.api.v2.controllers import rest +from designate.api.v2.views import blacklists as blacklists_view + +LOG = logging.getLogger(__name__) +central_api = central_rpcapi.CentralAPI() + + +class BlacklistsController(rest.RestController): + _view = blacklists_view.BlacklistsView() + _resource_schema = schema.Schema('v2', 'blacklist') + _collection_schema = schema.Schema('v2', 'blacklists') + + @pecan.expose(template='json:', content_type='application/json') + def get_one(self, blacklist_id): + """ Get Blacklist """ + + request = pecan.request + context = request.environ['context'] + + blacklist = central_api.get_blacklist(context, blacklist_id) + + return self._view.show(context, request, blacklist) + + @pecan.expose(template='json:', content_type='application/json') + def get_all(self, **params): + """ List all Blacklisted Zones """ + request = pecan.request + context = request.environ['context'] + + # Extract the pagination params + #marker = params.pop('marker', None) + #limit = int(params.pop('limit', 30)) + + # Extract any filter params + accepted_filters = ('pattern') + criterion = dict((k, params[k]) for k in accepted_filters + if k in params) + + blacklist = central_api.find_blacklists(context, criterion) + + return self._view.list(context, request, blacklist) + + @pecan.expose(template='json:', content_type='application/json') + def post_all(self): + """ Create Blacklisted Zone """ + request = pecan.request + response = pecan.response + context = request.environ['context'] + + body = request.body_dict + + # Validate the request conforms to the schema + self._resource_schema.validate(body) + + # Convert from APIv2 -> Central format + values = self._view.load(context, request, body) + + # Create the blacklist + blacklist = central_api.create_blacklist(context, values) + + response.status_int = 201 + + response.headers['Location'] = self._view._get_resource_href( + request, blacklist) + + # Prepare and return the response body + return self._view.show(context, request, blacklist) + + @pecan.expose(template='json:', content_type='application/json') + @pecan.expose(template='json:', content_type='application/json-patch+json') + def patch_one(self, blacklist_id): + """ Update Blacklisted Zone """ + request = pecan.request + context = request.environ['context'] + body = request.body_dict + response = pecan.response + + # Fetch the existing blacklisted zone + blacklist = central_api.get_blacklist(context, blacklist_id) + + # Convert to APIv2 Format + blacklist = self._view.show(context, request, blacklist) + + if request.content_type == 'application/json-patch+json': + raise NotImplemented('json-patch not implemented') + else: + blacklist = utils.deep_dict_merge(blacklist, body) + + # Validate the request conforms to the schema + self._resource_schema.validate(blacklist) + + values = self._view.load(context, request, body) + + blacklist = central_api.update_blacklist(context, + blacklist_id, values) + + response.status_int = 200 + + return self._view.show(context, request, blacklist) + + @pecan.expose(template=None, content_type='application/json') + def delete_one(self, blacklist_id): + """ Delete Blacklisted Zone """ + request = pecan.request + response = pecan.response + context = request.environ['context'] + + central_api.delete_blacklist(context, blacklist_id) + + response.status_int = 204 + + # NOTE: This is a hack and a half.. But Pecan needs it. + return '' diff --git a/designate/api/v2/controllers/root.py b/designate/api/v2/controllers/root.py index 2ccc4d817..d989ff89c 100644 --- a/designate/api/v2/controllers/root.py +++ b/designate/api/v2/controllers/root.py @@ -19,6 +19,7 @@ from designate.api.v2.controllers import reverse from designate.api.v2.controllers import schemas from designate.api.v2.controllers import tlds from designate.api.v2.controllers import zones +from designate.api.v2.controllers import blacklists LOG = logging.getLogger(__name__) @@ -33,3 +34,4 @@ class RootController(object): reverse = reverse.ReverseController() tlds = tlds.TldsController() zones = zones.ZonesController() + blacklists = blacklists.BlacklistsController() diff --git a/designate/api/v2/views/blacklists.py b/designate/api/v2/views/blacklists.py new file mode 100644 index 000000000..409e17108 --- /dev/null +++ b/designate/api/v2/views/blacklists.py @@ -0,0 +1,51 @@ +# Copyright 2014 Rackspace +# +# Author: Betsy Luzader +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from designate.api.v2.views import base as base_view +from designate.openstack.common import log as logging + + +LOG = logging.getLogger(__name__) + + +class BlacklistsView(base_view.BaseView): + """ Model a Blacklist API response as a python dictionary """ + + _resource_name = 'blacklist' + _collection_name = 'blacklists' + + def show_basic(self, context, request, blacklist): + """ Detailed view of a blacklisted zone """ + return { + "id": blacklist['id'], + "pattern": blacklist['pattern'], + "description": blacklist['description'], + "created_at": blacklist['created_at'], + "updated_at": blacklist['updated_at'], + "links": self._get_resource_links(request, blacklist) + } + + def load(self, context, request, body): + """ Extract a "central" compatible dict from an API call """ + result = {} + item = body[self._resource_name] + + # Copy keys which need no alterations + for k in ('id', 'pattern', 'description',): + if k in item: + result[k] = item[k] + + return result diff --git a/designate/central/__init__.py b/designate/central/__init__.py index 1eee91bef..3c29ee0e6 100644 --- a/designate/central/__init__.py +++ b/designate/central/__init__.py @@ -28,10 +28,6 @@ cfg.CONF.register_opts([ help='The storage driver to use'), cfg.ListOpt('enabled-notification-handlers', default=[], help='Enabled Notification Handlers'), - cfg.ListOpt('domain-name-blacklist', - default=['\\.arpa\\.$', '\\.novalocal\\.$', '\\.localhost\\.$', - '\\.localdomain\\.$', '\\.local\\.$'], - help='DNS domain name blacklist'), cfg.IntOpt('max_domain_name_len', default=255, help="Maximum domain name length"), cfg.IntOpt('max_recordset_name_len', default=255, diff --git a/designate/central/rpcapi.py b/designate/central/rpcapi.py index db1535791..3983d72b2 100644 --- a/designate/central/rpcapi.py +++ b/designate/central/rpcapi.py @@ -35,7 +35,7 @@ class CentralAPI(rpc_proxy.RpcProxy): 3.0 - RecordSet Changes 3.1 - Add floating ip ptr methods 3.2 - TLD Api changes - + 3.3 - Add methods for blacklisted domains """ def __init__(self, topic=None): topic = topic if topic else cfg.CONF.central_topic @@ -400,3 +400,41 @@ class CentralAPI(rpc_proxy.RpcProxy): msg = self.make_msg('update_floatingip', region=region, floatingip_id=floatingip_id, values=values) return self.call(context, msg) + + # Blacklisted Domain Methods + def create_blacklist(self, context, values): + LOG.info("create_blacklist: Calling central's create_blacklist") + msg = self.make_msg('create_blacklist', values=values) + + return self.call(context, msg, version='3.3') + + def get_blacklist(self, context, blacklist_id): + LOG.info("get_blacklist: Calling central's get_blacklist.") + msg = self.make_msg('get_blacklist', blacklist_id=blacklist_id) + + return self.call(context, msg, version='3.3') + + def find_blacklists(self, context, criterion=None): + LOG.info("find_blacklists: Calling central's find_blacklists.") + msg = self.make_msg('find_blacklists', criterion=criterion) + + return self.call(context, msg, version='3.3') + + def find_blacklist(self, context, criterion): + LOG.info("find_blacklist: Calling central's find_blacklist.") + msg = self.make_msg('find_blacklist', criterion=criterion) + + return self.call(context, msg, version='3.3') + + def update_blacklist(self, context, blacklist_id, values): + LOG.info("update_blacklist: Calling central's update_blacklist.") + msg = self.make_msg('update_blacklist', blacklist_id=blacklist_id, + values=values) + + return self.call(context, msg, version='3.3') + + def delete_blacklist(self, context, blacklist_id): + LOG.info("delete_blacklist: Calling central's delete blacklist.") + msg = self.make_msg('delete_blacklist', blacklist_id=blacklist_id) + + return self.call(context, msg, version='3.3') diff --git a/designate/central/service.py b/designate/central/service.py index 06add59b5..9e9e8a52a 100644 --- a/designate/central/service.py +++ b/designate/central/service.py @@ -45,7 +45,7 @@ def wrap_backend_call(): class Service(rpc_service.Service): - RPC_API_VERSION = '3.2' + RPC_API_VERSION = '3.3' def __init__(self, *args, **kwargs): backend_driver = cfg.CONF['service:central'].backend_driver @@ -203,11 +203,12 @@ class Service(rpc_service.Service): """ Ensures the provided domain_name is not blacklisted. """ - blacklists = cfg.CONF['service:central'].domain_name_blacklist + + blacklists = self.storage_api.find_blacklists(context) for blacklist in blacklists: - if bool(re.search(blacklist, domain_name)): - return blacklist + if bool(re.search(blacklist["pattern"], domain_name)): + return True return False @@ -1328,3 +1329,56 @@ class Service(rpc_service.Service): elif isinstance(values['ptrdname'], basestring): return self._set_floatingip_reverse( context, region, floatingip_id, values) + + # Blacklisted Domains + def create_blacklist(self, context, values): + policy.check('create_blacklist', context) + + with self.storage_api.create_blacklist(context, values) as blacklist: + pass # NOTE: No other systems need updating + + self.notifier.info(context, 'dns.blacklist.create', blacklist) + + return blacklist + + def get_blacklist(self, context, blacklist_id): + policy.check('get_blacklist', context) + + blacklist = self.storage_api.get_blacklist(context, blacklist_id) + + return blacklist + + def find_blacklists(self, context, criterion=None): + policy.check('find_blacklists', context) + + blacklists = self.storage_api.find_blacklists(context, criterion) + + return blacklists + + def find_blacklist(self, context, criterion): + policy.check('find_blacklist', context) + + blacklist = self.storage_api.find_blacklist(context, criterion) + + return blacklist + + def update_blacklist(self, context, blacklist_id, values): + policy.check('update_blacklist', context) + + with self.storage_api.update_blacklist(context, + blacklist_id, + values) as blacklist: + pass # NOTE: No other systems need updating + + self.notifier.info(context, 'dns.blacklist.update', blacklist) + + return blacklist + + def delete_blacklist(self, context, blacklist_id): + policy.check('delete_blacklist', context) + + with self.storage_api.delete_blacklist(context, + blacklist_id) as blacklist: + pass # NOTE: No other systems need updating + + self.notifier.info(context, 'dns.blacklist.delete', blacklist) diff --git a/designate/exceptions.py b/designate/exceptions.py index 79b20dca4..b018c5b6b 100644 --- a/designate/exceptions.py +++ b/designate/exceptions.py @@ -160,6 +160,10 @@ class DuplicateRecord(Duplicate): error_type = 'duplicate_record' +class DuplicateBlacklist(Duplicate): + error_type = 'duplicate_blacklist' + + class NotFound(Base): error_code = 404 error_type = 'not_found' @@ -177,6 +181,10 @@ class TsigKeyNotFound(NotFound): error_type = 'tsigkey_not_found' +class BlacklistNotFound(NotFound): + error_type = 'blacklist_not_found' + + class DomainNotFound(NotFound): error_type = 'domain_not_found' diff --git a/designate/resources/schemas/v2/blacklist.json b/designate/resources/schemas/v2/blacklist.json new file mode 100644 index 000000000..8eab175fe --- /dev/null +++ b/designate/resources/schemas/v2/blacklist.json @@ -0,0 +1,63 @@ +{ + "$schema": "http://json-schema.org/draft-04/hyper-schema", + + "id": "blacklist", + + "title": "blacklist", + "description": "Blacklisted Zone", + "additionalProperties": false, + + "required": ["blacklist"], + + "properties": { + "blacklist": { + "type": "object", + "additionalProperties": false, + "required": ["pattern"], + + "properties":{ + "id": { + "type": "string", + "description": "Blacklisted Zone Identifier", + "pattern": "^([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}$", + "readonly": true + }, + "pattern": { + "type": "string", + "description": "Regex for blacklisted zone name", + "format": "regex", + "maxLength": 512, + "required": true + }, + "created_at": { + "type": "string", + "description": "Date and time of blacklisted zone creation", + "format": "date-time", + "readonly": true + }, + "description": { + "type": ["string", "null"], + "description": "Description for the blacklisted zone", + "maxLength": 160 + }, + "updated_at": { + "type": ["string", "null"], + "description": "Date and time of last blacklisted zone update", + "format": "date-time", + "readonly": true + }, + "links": { + "type": "object", + "additionalProperties": false, + + "properties": { + "self": { + "type": "string", + "format": "url" + } + } + } + } + } + } +} diff --git a/designate/resources/schemas/v2/blacklists.json b/designate/resources/schemas/v2/blacklists.json new file mode 100644 index 000000000..b16129728 --- /dev/null +++ b/designate/resources/schemas/v2/blacklists.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json-schema.org/draft-04/hyper-schema", + + "id": "blacklist", + + "title": "blacklist", + "description": "Blacklisted Zone", + "additionalProperties": false, + + "required": ["blacklists"], + + "properties": { + "blacklists": { + "type": "array", + "description": "Blacklist", + "items": {"$ref": "blacklist#/properties/blacklist"} + }, + "links": { + "type": "object", + "additionalProperties": false, + + "properties": { + "self": { + "type": "string", + "format": "url" + }, + "next": { + "type": ["string", "null"], + "format": "url" + }, + "previous": { + "type": ["string", "null"], + "format": "url" + } + } + } + } +} diff --git a/designate/storage/api.py b/designate/storage/api.py index d3ef49862..083c6c5dc 100644 --- a/designate/storage/api.py +++ b/designate/storage/api.py @@ -677,6 +677,93 @@ class StorageAPI(object): """ return self.storage.count_records(context, criterion) + @contextlib.contextmanager + def create_blacklist(self, context, values): + """ + Create a new Blacklisted Domain. + + :param context: RPC Context. + :param values: Values to create the new Blacklist from. + """ + self.storage.begin() + + try: + blacklist = self.storage.create_blacklist(context, values) + yield blacklist + except Exception: + with excutils.save_and_reraise_exception(): + self.storage.rollback() + else: + self.storage.commit() + + def get_blacklist(self, context, blacklist_id): + """ + Get a Blacklist via its ID. + + :param context: RPC Context. + :param blacklist_id: ID of the Blacklisted Domain. + """ + return self.storage.get_blacklist(context, blacklist_id) + + def find_blacklists(self, context, criterion=None): + """ + Find all Blacklisted Domains + + :param context: RPC Context. + :param criterion: Criteria to filter by. + """ + return self.storage.find_blacklists(context, criterion) + + def find_blacklist(self, context, criterion): + """ + Find a single Blacklisted Domain. + + :param context: RPC Context. + :param criterion: Criteria to filter by. + """ + return self.storage.find_blacklist(context, criterion) + + @contextlib.contextmanager + def update_blacklist(self, context, blacklist_id, values): + """ + Update a Blacklisted Domain via ID. + + :param context: RPC Context. + :param blacklist_id: Values to update the Blacklist with + :param values: Values to update the Blacklist from. + """ + self.storage.begin() + + try: + blacklist = self.storage.update_blacklist(context, + blacklist_id, + values) + yield blacklist + except Exception: + with excutils.save_and_reraise_exception(): + self.storage.rollback() + else: + self.storage.commit() + + @contextlib.contextmanager + def delete_blacklist(self, context, blacklist_id): + """ + Delete a Blacklisted Domain + + :param context: RPC Context. + :param blacklist_id: Blacklist ID to delete. + """ + self.storage.begin() + + try: + yield self.storage.get_blacklist(context, blacklist_id) + self.storage.delete_blacklist(context, blacklist_id) + except Exception: + with excutils.save_and_reraise_exception(): + self.storage.rollback() + else: + self.storage.commit() + def ping(self, context): """ Ping the Storage connection """ return self.storage.ping(context) diff --git a/designate/storage/base.py b/designate/storage/base.py index bb08de120..11f650d8c 100644 --- a/designate/storage/base.py +++ b/designate/storage/base.py @@ -443,6 +443,61 @@ class Storage(DriverPlugin): :param criterion: Criteria to filter by. """ + @abc.abstractmethod + def create_blacklist(self, context, values): + """ + Create a Blacklist. + + :param context: RPC Context. + :param values: Values to create the new Blacklist from. + """ + + @abc.abstractmethod + def get_blacklist(self, context, blacklist_id): + """ + Get a Blacklist via ID. + + :param context: RPC Context. + :param blacklist_id: Blacklist ID to get. + """ + + @abc.abstractmethod + def find_blacklists(self, context, criterion): + """ + Find Blacklists + + :param context: RPC Context. + :param criterion: Criteria to filter by. + """ + + @abc.abstractmethod + def find_blacklist(self, context, criterion): + """ + Find a single Blacklist. + + :param context: RPC Context. + :param criterion: Criteria to filter by. + """ + + @abc.abstractmethod + def update_blacklist(self, context, blacklist_id, values): + """ + Update a Blacklist via ID + + :param context: RPC Context. + :param blacklist_id: Blacklist ID to update. + :param values: Values to update the Blacklist from + """ + + @abc.abstractmethod + def delete_blacklist(self, context, blacklist_id): + """ + Delete a Blacklist via ID. + + :param context: RPC Context. + :param blacklist_id: Delete a Blacklist via ID + """ + def ping(self, context): """ Ping the Storage connection """ return { diff --git a/designate/storage/impl_sqlalchemy/__init__.py b/designate/storage/impl_sqlalchemy/__init__.py index 72b60600b..d49f6112d 100644 --- a/designate/storage/impl_sqlalchemy/__init__.py +++ b/designate/storage/impl_sqlalchemy/__init__.py @@ -546,6 +546,63 @@ class SQLAlchemyStorage(base.Storage): query = self._apply_criterion(models.Record, query, criterion) return query.count() + # + # Blacklist Methods + # + def _find_blacklist(self, context, criterion, one=False): + try: + return self._find(models.Blacklists, context, criterion, one) + except exceptions.NotFound: + raise exceptions.BlacklistNotFound() + + def create_blacklist(self, context, values): + blacklist = models.Blacklists() + + blacklist.update(values) + + try: + blacklist.save(self.session) + except exceptions.Duplicate: + raise exceptions.DuplicateBlacklist() + + return dict(blacklist) + + def find_blacklists(self, context, criterion=None): + blacklists = self._find_blacklist(context, criterion) + + return [dict(b) for b in blacklists] + + def get_blacklist(self, context, blacklist_id): + blacklist = self._find_blacklist(context, + {'id': blacklist_id}, one=True) + + return dict(blacklist) + + def find_blacklist(self, context, criterion): + blacklist = self._find_blacklist(context, criterion, one=True) + + return dict(blacklist) + + def update_blacklist(self, context, blacklist_id, values): + blacklist = self._find_blacklist(context, {'id': blacklist_id}, + one=True) + + blacklist.update(values) + + try: + blacklist.save(self.session) + except exceptions.Duplicate: + raise exceptions.DuplicateBlacklist() + + return dict(blacklist) + + def delete_blacklist(self, context, blacklist_id): + + blacklist = self._find_blacklist(context, {'id': blacklist_id}, + one=True) + + blacklist.delete(self.session) + # diagnostics def ping(self, context): start_time = time.time() diff --git a/designate/storage/impl_sqlalchemy/migrate_repo/versions/038_add_blacklists_table.py b/designate/storage/impl_sqlalchemy/migrate_repo/versions/038_add_blacklists_table.py new file mode 100644 index 000000000..b914ba3b7 --- /dev/null +++ b/designate/storage/impl_sqlalchemy/migrate_repo/versions/038_add_blacklists_table.py @@ -0,0 +1,54 @@ +# Copyright (c) 2014 Rackspace Hosting +# All Rights Reserved. +# +# Author: Betsy Luzader +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +from sqlalchemy import Integer, String, DateTime +from sqlalchemy.schema import Table, Column, MetaData +from designate.openstack.common import timeutils +from designate.openstack.common.uuidutils import generate_uuid +from designate.sqlalchemy.types import UUID + +meta = MetaData() + +blacklists = Table( + 'blacklists', + meta, + Column('id', UUID(), default=generate_uuid, + primary_key=True), + Column('created_at', DateTime(), + default=timeutils.utcnow), + Column('updated_at', DateTime(), + onupdate=timeutils.utcnow), + Column('version', Integer(), default=1, + nullable=False), + Column('pattern', String(512), nullable=False, + unique=True), + Column('description', String(160), + nullable=True), + + mysql_engine='INNODB', + mysql_charset='utf8') + + +def upgrade(migrate_engine): + meta.bind = migrate_engine + + blacklists.create() + + +def downgrade(migrate_engine): + meta.bind = migrate_engine + + blacklists.drop() diff --git a/designate/storage/impl_sqlalchemy/models.py b/designate/storage/impl_sqlalchemy/models.py index a02e016cb..ae1acbe5f 100644 --- a/designate/storage/impl_sqlalchemy/models.py +++ b/designate/storage/impl_sqlalchemy/models.py @@ -188,3 +188,10 @@ class TsigKey(Base): algorithm = Column(Enum(name='tsig_algorithms', *TSIG_ALGORITHMS), nullable=False) secret = Column(String(255), nullable=False) + + +class Blacklists(Base): + __tablename__ = 'blacklists' + + pattern = Column(String(512), nullable=False, unique=True) + description = Column(Unicode(160), nullable=True) diff --git a/designate/tests/__init__.py b/designate/tests/__init__.py index a5ef70772..721bccc29 100644 --- a/designate/tests/__init__.py +++ b/designate/tests/__init__.py @@ -220,6 +220,15 @@ class TestCase(test.BaseTestCase): {'ptrdname': 'srv1.example.net.'} ] + blacklist_fixtures = [{ + 'pattern': 'blacklisted.com.', + 'description': 'This is a comment', + }, { + 'pattern': 'blacklisted.net.' + }, { + 'pattern': 'blacklisted.org.' + }] + def setUp(self): super(TestCase, self).setUp() @@ -384,6 +393,11 @@ class TestCase(test.BaseTestCase): with open(path) as zonefile: return zonefile.read() + def get_blacklist_fixture(self, fixture=0, values={}): + _values = copy.copy(self.blacklist_fixtures[fixture]) + _values.update(values) + return _values + def create_quota(self, **kwargs): context = kwargs.pop('context', self.admin_context) fixture = kwargs.pop('fixture', 0) @@ -465,6 +479,13 @@ class TestCase(test.BaseTestCase): recordset['id'], values=values) + def create_blacklist(self, **kwargs): + context = kwargs.pop('context', self.admin_context) + fixture = kwargs.pop('fixture', 0) + + values = self.get_blacklist_fixture(fixture=fixture, values=kwargs) + return self.central_service.create_blacklist(context, values=values) + def _skip_decorator(func): @functools.wraps(func) diff --git a/designate/tests/test_central/test_service.py b/designate/tests/test_central/test_service.py index 65352a877..cff711921 100644 --- a/designate/tests/test_central/test_service.py +++ b/designate/tests/test_central/test_service.py @@ -66,8 +66,11 @@ class CentralServiceTest(CentralTestCase): context, domain, 'a.example.COM.') def test_is_blacklisted_domain_name(self): - self.config(domain_name_blacklist=['^example.org.$', 'net.$'], - group='service:central') + # Create blacklisted zones with specific names + self.create_blacklist(pattern='example.org.') + self.create_blacklist(pattern='example.net.') + self.create_blacklist(pattern='^blacklisted.org.$') + self.create_blacklist(pattern='com.$') # Set the policy to reject the authz self.policy({'use_blacklisted_domain': '!'}) @@ -78,18 +81,28 @@ class CentralServiceTest(CentralTestCase): context, 'org.') self.assertFalse(result) + # Subdomains should not be allowed from a blacklisted domain result = self.central_service._is_blacklisted_domain_name( context, 'www.example.org.') - self.assertFalse(result) + self.assertTrue(result) result = self.central_service._is_blacklisted_domain_name( context, 'example.org.') self.assertTrue(result) + # Check for blacklisted domains containing regexps result = self.central_service._is_blacklisted_domain_name( context, 'example.net.') self.assertTrue(result) + result = self.central_service._is_blacklisted_domain_name( + context, 'example.com.') + self.assertTrue(result) + + result = self.central_service._is_blacklisted_domain_name( + context, 'blacklisted.org.') + self.assertTrue(result) + def test_is_subdomain(self): context = self.get_context() @@ -508,8 +521,8 @@ class CentralServiceTest(CentralTestCase): self.central_service.create_domain(context, values=values) def test_create_blacklisted_domain_success(self): - self.config(domain_name_blacklist=['^blacklisted.com.$'], - group='service:central') + # Create blacklisted zone using default values + self.create_blacklist() # Set the policy to accept the authz self.policy({'use_blacklisted_domain': '@'}) @@ -522,7 +535,7 @@ class CentralServiceTest(CentralTestCase): # Create a server self.create_server() - # Create a domain + # Create a zone that is blacklisted domain = self.central_service.create_domain( self.admin_context, values=values) @@ -532,8 +545,7 @@ class CentralServiceTest(CentralTestCase): self.assertEqual(domain['email'], values['email']) def test_create_blacklisted_domain_fail(self): - self.config(domain_name_blacklist=['^blacklisted.com.$'], - group='service:central') + self.create_blacklist() # Set the policy to reject the authz self.policy({'use_blacklisted_domain': '!'}) @@ -1705,3 +1717,101 @@ class CentralServiceTest(CentralTestCase): self.central_service.get_floatingip( context, fip['region'], fip['id']) + + # Blacklist Tests + def test_create_blacklist(self): + values = self.get_blacklist_fixture(fixture=0) + + blacklist = self.create_blacklist(fixture=0) + + # Verify all values have been set correctly + self.assertIsNotNone(blacklist['id']) + self.assertEqual(blacklist['pattern'], values['pattern']) + self.assertEqual(blacklist['description'], values['description']) + + def test_get_blacklist(self): + # Create a blacklisted zone + expected = self.create_blacklist(fixture=0) + + # Retrieve it, and verify it is the same + blacklist = self.central_service.get_blacklist( + self.admin_context, expected['id']) + + self.assertEqual(blacklist['id'], expected['id']) + self.assertEqual(blacklist['pattern'], expected['pattern']) + self.assertEqual(blacklist['description'], expected['description']) + + def test_find_blacklists(self): + # Verify there are no blacklisted zones to start with + blacklists = self.central_service.find_blacklists( + self.admin_context) + + self.assertEqual(len(blacklists), 0) + + # Create a single blacklisted zone + self.create_blacklist() + + # Verify we can retrieve the newly created blacklist + blacklists = self.central_service.find_blacklists( + self.admin_context) + values1 = self.get_blacklist_fixture(fixture=0) + + self.assertEqual(len(blacklists), 1) + self.assertEqual(blacklists[0]['pattern'], values1['pattern']) + + # Create a second blacklisted zone + self.create_blacklist(fixture=1) + + # Verify we can retrieve both blacklisted zones + blacklists = self.central_service.find_blacklists( + self.admin_context) + + values2 = self.get_blacklist_fixture(fixture=1) + + self.assertEqual(len(blacklists), 2) + self.assertEqual(blacklists[0]['pattern'], values1['pattern']) + self.assertEqual(blacklists[1]['pattern'], values2['pattern']) + + def test_find_blacklist(self): + #Create a blacklisted zone + expected = self.create_blacklist(fixture=0) + + # Retrieve the newly created blacklist + blacklist = self.central_service.find_blacklist( + self.admin_context, {'id': expected['id']}) + + self.assertEqual(blacklist['pattern'], expected['pattern']) + self.assertEqual(blacklist['description'], expected['description']) + + def test_update_blacklist(self): + # Create a blacklisted zone + expected = self.create_blacklist(fixture=0) + new_comment = "This is a different comment." + + # Update the blacklist + updated_values = dict( + description=new_comment + ) + self.central_service.update_blacklist(self.admin_context, + expected['id'], + updated_values) + + # Fetch the blacklist + blacklist = self.central_service.get_blacklist(self.admin_context, + expected['id']) + + # Verify that the record was updated correctly + self.assertEqual(blacklist['description'], new_comment) + + def test_delete_blacklist(self): + # Create a blacklisted zone + blacklist = self.create_blacklist() + + # Delete the blacklist + self.central_service.delete_blacklist(self.admin_context, + blacklist['id']) + + # Try to fetch the blacklist to verify an exception is raised + with testtools.ExpectedException(exceptions.BlacklistNotFound): + self.central_service.get_blacklist(self.admin_context, + blacklist['id']) diff --git a/doc/source/examples/basic-config-sample.conf b/doc/source/examples/basic-config-sample.conf index 2ec371401..a88a2e3c7 100644 --- a/doc/source/examples/basic-config-sample.conf +++ b/doc/source/examples/basic-config-sample.conf @@ -32,9 +32,6 @@ root_helper = sudo # Driver used for backend communication (e.g. fake, rpc, bind9, powerdns) backend_driver = powerdns -# List of blacklist domain name regexes -#domain_name_blacklist = \.arpa\.$, \.novalocal\.$, \.localhost\.$, \.localdomain\.$, \.local\.$ - # Maximum domain name length max_domain_name_len = 255 diff --git a/etc/designate/designate.conf.sample b/etc/designate/designate.conf.sample index 6c07ce7d8..0b1fe9b5d 100644 --- a/etc/designate/designate.conf.sample +++ b/etc/designate/designate.conf.sample @@ -35,9 +35,6 @@ root_helper = sudo # Driver used for backend communication (e.g. fake, rpc, bind9, powerdns) #backend_driver = fake -# List of blacklist domain name regexes -#domain_name_blacklist = \.arpa\.$, \.novalocal\.$, \.localhost\.$, \.localdomain\.$, \.local\.$ - # Maximum domain name length #max_domain_name_len = 255 diff --git a/etc/designate/policy.json b/etc/designate/policy.json index d274b25ef..0cb9f720c 100644 --- a/etc/designate/policy.json +++ b/etc/designate/policy.json @@ -53,6 +53,13 @@ "count_records": "rule:admin_or_owner", "use_sudo": "rule:admin", + + "create_blacklist": "rule:admin", + "find_blacklist": "rule:admin", + "find_blacklists": "rule:admin", + "get_blacklist": "rule:admin", + "update_blacklist": "rule:admin", + "delete_blacklist": "rule:admin", "use_blacklisted_domain": "rule:admin", "diagnostics_ping": "rule:admin",