diff --git a/designate/api/middleware.py b/designate/api/middleware.py index 364834ae0..0bd232a6c 100644 --- a/designate/api/middleware.py +++ b/designate/api/middleware.py @@ -92,6 +92,17 @@ class ContextMiddleware(base.Middleware): strutils.bool_from_string(params['all_tenants']) else: ctxt.all_tenants = False + if 'edit_managed_records' in params: + ctxt.edit_managed_records = \ + strutils.bool_from_string(params['edit_managed_records']) + + elif headers.get('X-Designate-Edit-Managed-Records'): + ctxt.edit_managed_records = \ + strutils.bool_from_string( + headers.get('X-Designate-Edit-Managed-Records')) + + else: + ctxt.edit_managed_records = False finally: request.environ['context'] = ctxt diff --git a/designate/api/v1/records.py b/designate/api/v1/records.py index 44f4c4132..b7375c11b 100644 --- a/designate/api/v1/records.py +++ b/designate/api/v1/records.py @@ -206,8 +206,8 @@ def update_record(domain_id, record_id): criterion = {'domain_id': domain_id, 'id': record_id} record = central_api.find_record(context, criterion) - # Cannot update a managed record via the API. - if record['managed'] is True: + # TODO(graham): Move this further down the stack + if record.managed and not context.edit_managed_records: raise exceptions.BadRequest('Managed records may not be updated') # Find the associated recordset @@ -263,10 +263,6 @@ def delete_record(domain_id, record_id): criterion = {'domain_id': domain_id, 'id': record_id} record = central_api.find_record(context, criterion) - # Cannot delete a managed record via the API. - if record['managed'] is True: - raise exceptions.BadRequest('Managed records may not be deleted') - central_api.delete_record( context, domain_id, record['recordset_id'], record_id) diff --git a/designate/api/v2/controllers/recordsets.py b/designate/api/v2/controllers/recordsets.py index d0d706f74..5ebf3b567 100644 --- a/designate/api/v2/controllers/recordsets.py +++ b/designate/api/v2/controllers/recordsets.py @@ -140,6 +140,10 @@ class RecordSetsController(rest.RestController): recordset = self.central_api.get_recordset(context, zone_id, recordset_id) + # TODO(graham): Move this further down the stack + if recordset.managed and not context.edit_managed_records: + raise exceptions.BadRequest('Managed records may not be updated') + # SOA recordsets cannot be updated manually if recordset['type'] == 'SOA': raise exceptions.BadRequest( diff --git a/designate/central/service.py b/designate/central/service.py index 82acb07fe..a46b5fa35 100644 --- a/designate/central/service.py +++ b/designate/central/service.py @@ -1232,6 +1232,9 @@ class Service(service.RPCService, service.Service): policy.check('update_recordset', context, target) + if recordset.managed and not context.edit_managed_records: + raise exceptions.BadRequest('Managed records may not be updated') + recordset, domain = self._update_recordset_in_storage( context, domain, recordset, increment_serial=increment_serial) @@ -1299,6 +1302,9 @@ class Service(service.RPCService, service.Service): policy.check('delete_recordset', context, target) + if recordset.managed and not context.edit_managed_records: + raise exceptions.BadRequest('Managed records may not be updated') + recordset, domain = self._delete_recordset_in_storage( context, domain, recordset, increment_serial=increment_serial) @@ -1472,6 +1478,9 @@ class Service(service.RPCService, service.Service): policy.check('update_record', context, target) + if recordset.managed and not context.edit_managed_records: + raise exceptions.BadRequest('Managed records may not be updated') + record, domain = self._update_record_in_storage( context, domain, record, increment_serial=increment_serial) @@ -1530,6 +1539,9 @@ class Service(service.RPCService, service.Service): policy.check('delete_record', context, target) + if recordset.managed and not context.edit_managed_records: + raise exceptions.BadRequest('Managed records may not be updated') + record, domain = self._delete_record_in_storage( context, domain, record, increment_serial=increment_serial) @@ -1652,6 +1664,7 @@ class Service(service.RPCService, service.Service): elevated_context = context.elevated() elevated_context.all_tenants = True + elevated_context.edit_managed_records = True criterion = { 'managed': True, @@ -1692,6 +1705,7 @@ class Service(service.RPCService, service.Service): """ elevated_context = context.elevated() elevated_context.all_tenants = True + elevated_context.edit_managed_records = True if records > 0: for r in records: @@ -1782,6 +1796,7 @@ class Service(service.RPCService, service.Service): """ elevated_context = context.elevated() elevated_context.all_tenants = True + elevated_context.edit_managed_records = True tenant_fips = self._list_floatingips(context) @@ -1817,6 +1832,7 @@ class Service(service.RPCService, service.Service): elevated_context = context.elevated() elevated_context.all_tenants = True + elevated_context.edit_managed_records = True tenant_fips = self._list_floatingips(context, region=region) @@ -1917,6 +1933,7 @@ class Service(service.RPCService, service.Service): """ elevated_context = context.elevated() elevated_context.all_tenants = True + elevated_context.edit_managed_records = True criterion = { 'managed_resource_id': floatingip_id, diff --git a/designate/context.py b/designate/context.py index 2ba86ba15..dfe98ddeb 100644 --- a/designate/context.py +++ b/designate/context.py @@ -30,13 +30,16 @@ class DesignateContext(context.RequestContext): _all_tenants = False _abandon = None original_tenant = None + _edit_managed_records = False def __init__(self, auth_token=None, user=None, tenant=None, domain=None, user_domain=None, project_domain=None, is_admin=False, read_only=False, show_deleted=False, request_id=None, resource_uuid=None, overwrite=True, roles=None, service_catalog=None, all_tenants=False, abandon=None, - tsigkey_id=None, user_identity=None, original_tenant=None): + tsigkey_id=None, user_identity=None, original_tenant=None, + edit_managed_records=False): + # NOTE: user_identity may be passed in, but will be silently dropped as # it is a generated field based on several others. super(DesignateContext, self).__init__( @@ -61,6 +64,7 @@ class DesignateContext(context.RequestContext): self.all_tenants = all_tenants self.abandon = abandon + self.edit_managed_records = edit_managed_records def deepcopy(self): d = self.to_dict() @@ -93,6 +97,7 @@ class DesignateContext(context.RequestContext): 'service_catalog': self.service_catalog, 'all_tenants': self.all_tenants, 'abandon': self.abandon, + 'edit_managed_records': self.edit_managed_records, 'tsigkey_id': self.tsigkey_id }) @@ -167,6 +172,16 @@ class DesignateContext(context.RequestContext): policy.check('abandon_domain', self) self._abandon = value + @property + def edit_managed_records(self): + return self._edit_managed_records + + @edit_managed_records.setter + def edit_managed_records(self, value): + if value: + policy.check('edit_managed_records', self) + self._edit_managed_records = value + def get_current(): return context.get_current() diff --git a/designate/notification_handler/base.py b/designate/notification_handler/base.py index 04672189b..48df92f16 100644 --- a/designate/notification_handler/base.py +++ b/designate/notification_handler/base.py @@ -132,7 +132,9 @@ class BaseAddressHandler(NotificationHandler): LOG.debug('Event data: %s' % data) data['domain'] = domain['name'] - context = DesignateContext.get_admin_context(all_tenants=True) + context = DesignateContext().elevated() + context.all_tenants = True + context.edit_managed_records = True for addr in addresses: event_data = data.copy() @@ -178,7 +180,9 @@ class BaseAddressHandler(NotificationHandler): 'to remove managed=False')) criterion = criterion or {} - context = DesignateContext.get_admin_context(all_tenants=True) + context = DesignateContext().elevated() + context.all_tenants = True + context.edit_managed_records = True criterion.update({'domain_id': cfg.CONF[self.name].domain_id}) diff --git a/designate/objects/recordset.py b/designate/objects/recordset.py index e46a5588f..a18e7c8e3 100644 --- a/designate/objects/recordset.py +++ b/designate/objects/recordset.py @@ -48,6 +48,14 @@ class RecordSet(base.DictObjectMixin, base.PersistentObjectMixin, action = 'UPDATE' return action + @property + def managed(self): + managed = False + for record in self.records: + if record.managed: + return True + return managed + @property def status(self): # Return the worst status in order of ERROR, PENDING, ACTIVE diff --git a/designate/tests/test_context.py b/designate/tests/test_context.py index e0f269978..23096d87e 100644 --- a/designate/tests/test_context.py +++ b/designate/tests/test_context.py @@ -51,3 +51,18 @@ class TestDesignateContext(TestCase): ctxt = context.DesignateContext(user='12345', tenant='54321') with testtools.ExpectedException(exceptions.Forbidden): ctxt.all_tenants = True + + def test_edit_managed_records(self): + ctxt = context.DesignateContext(user='12345', tenant='54321') + admin_ctxt = ctxt.elevated() + + admin_ctxt.edit_managed_records = True + + self.assertFalse(ctxt.is_admin) + self.assertTrue(admin_ctxt.is_admin) + self.assertTrue(admin_ctxt.edit_managed_records) + + def test_edit_managed_records_failure(self): + ctxt = context.DesignateContext(user='12345', tenant='54321') + with testtools.ExpectedException(exceptions.Forbidden): + ctxt.edit_managed_records = True diff --git a/doc/source/rest.rst b/doc/source/rest.rst index bc0465158..95a882b4e 100644 --- a/doc/source/rest.rst +++ b/doc/source/rest.rst @@ -46,6 +46,16 @@ tutorial`_ for more info. .. _cURL: http://curl.haxx.se/ .. _cURL tutorial: http://curl.haxx.se/docs/manual.html +HTTP Headers +============ + +These headers work for all APIs + +* X-Designate-Edit-Managed-Records + - Allows admins (or users with the right role) to modify managed records (records created by designate-sink / reverse floating ip API) +* X-Auth-All-Projects + - Allows admins (or users with the right role) to view and edit zones / recordsets for all tenants + API Versions ============ diff --git a/etc/designate/policy.json b/etc/designate/policy.json index 2642f5234..a085e2da4 100644 --- a/etc/designate/policy.json +++ b/etc/designate/policy.json @@ -15,6 +15,8 @@ "all_tenants": "rule:admin", + "edit_managed_records" : "rule:admin", + "use_low_ttl": "rule:admin", "get_quotas": "rule:admin_or_owner",