Restrict editing of managed records to policy based ACL

* Added new context param (edit_managed_records)
* Added new HTTP Header (X-Designate-Edit-Managed-Records:True)
* Added new HTTP Query param (?edit_managed_records=True)
* Added policy check (edit_managed_records), defaulting to rule:admin

Change-Id: Ib68369fd7302384fd4fbd1396baa513265edb0a0
Closes-Bug: #1441283
This commit is contained in:
Graham Hayes 2015-04-08 14:26:12 +01:00
parent 7dd7682946
commit a4f3ad90b9
10 changed files with 91 additions and 9 deletions

View File

@ -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

View File

@ -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)

View File

@ -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(

View File

@ -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,

View File

@ -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()

View File

@ -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})

View File

@ -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

View File

@ -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

View File

@ -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
============

View File

@ -15,6 +15,8 @@
"all_tenants": "rule:admin",
"edit_managed_records" : "rule:admin",
"use_low_ttl": "rule:admin",
"get_quotas": "rule:admin_or_owner",