Add 'tasks_api_access' policy
The Tasks API was made admin-only in Mitaka to prevent it from being
exposed directly to end users. The interoperable image import
process introduced in Pike uses the tasks engine to perform the
import. This patch introduces a new policy, 'tasks_api_access',
that determines whether a user can make Tasks API calls.
The currently existing task-related policies are retained so that
operators can have fine-grained control over tasks. With this
new policy, operators can restrict Tasks API access to admins,
while at the same time, admin-level credentials are not required
for glance to perform task-related functions on behalf of users.
Change-Id: I3f66f7efa7c377d999a88457fc6492701a894f34
Closes-bug: #1711468
(cherry picked from commit b90ad2524f
)
This commit is contained in:
parent
3a281182c6
commit
f6d384f184
@ -26,10 +26,11 @@
|
|||||||
|
|
||||||
"manage_image_cache": "role:admin",
|
"manage_image_cache": "role:admin",
|
||||||
|
|
||||||
"get_task": "role:admin",
|
"get_task": "",
|
||||||
"get_tasks": "role:admin",
|
"get_tasks": "",
|
||||||
"add_task": "role:admin",
|
"add_task": "",
|
||||||
"modify_task": "role:admin",
|
"modify_task": "",
|
||||||
|
"tasks_api_access": "role:admin",
|
||||||
|
|
||||||
"deactivate": "",
|
"deactivate": "",
|
||||||
"reactivate": "",
|
"reactivate": "",
|
||||||
|
@ -67,6 +67,8 @@ class TasksController(object):
|
|||||||
|
|
||||||
@debtcollector.removals.remove(message=_DEPRECATION_MESSAGE)
|
@debtcollector.removals.remove(message=_DEPRECATION_MESSAGE)
|
||||||
def create(self, req, task):
|
def create(self, req, task):
|
||||||
|
# NOTE(rosmaita): access to this call is enforced in the deserializer
|
||||||
|
|
||||||
task_factory = self.gateway.get_task_factory(req.context)
|
task_factory = self.gateway.get_task_factory(req.context)
|
||||||
executor_factory = self.gateway.get_task_executor_factory(req.context)
|
executor_factory = self.gateway.get_task_executor_factory(req.context)
|
||||||
task_repo = self.gateway.get_task_repo(req.context)
|
task_repo = self.gateway.get_task_repo(req.context)
|
||||||
@ -88,6 +90,8 @@ class TasksController(object):
|
|||||||
@debtcollector.removals.remove(message=_DEPRECATION_MESSAGE)
|
@debtcollector.removals.remove(message=_DEPRECATION_MESSAGE)
|
||||||
def index(self, req, marker=None, limit=None, sort_key='created_at',
|
def index(self, req, marker=None, limit=None, sort_key='created_at',
|
||||||
sort_dir='desc', filters=None):
|
sort_dir='desc', filters=None):
|
||||||
|
# NOTE(rosmaita): access to this call is enforced in the deserializer
|
||||||
|
|
||||||
result = {}
|
result = {}
|
||||||
if filters is None:
|
if filters is None:
|
||||||
filters = {}
|
filters = {}
|
||||||
@ -115,6 +119,7 @@ class TasksController(object):
|
|||||||
|
|
||||||
@debtcollector.removals.remove(message=_DEPRECATION_MESSAGE)
|
@debtcollector.removals.remove(message=_DEPRECATION_MESSAGE)
|
||||||
def get(self, req, task_id):
|
def get(self, req, task_id):
|
||||||
|
_enforce_access_policy(self.policy, req)
|
||||||
try:
|
try:
|
||||||
task_repo = self.gateway.get_task_repo(req.context)
|
task_repo = self.gateway.get_task_repo(req.context)
|
||||||
task = task_repo.get(task_id)
|
task = task_repo.get(task_id)
|
||||||
@ -135,6 +140,7 @@ class TasksController(object):
|
|||||||
|
|
||||||
@debtcollector.removals.remove(message=_DEPRECATION_MESSAGE)
|
@debtcollector.removals.remove(message=_DEPRECATION_MESSAGE)
|
||||||
def delete(self, req, task_id):
|
def delete(self, req, task_id):
|
||||||
|
_enforce_access_policy(self.policy, req)
|
||||||
msg = (_("This operation is currently not permitted on Glance Tasks. "
|
msg = (_("This operation is currently not permitted on Glance Tasks. "
|
||||||
"They are auto deleted after reaching the time based on "
|
"They are auto deleted after reaching the time based on "
|
||||||
"their expires_at property."))
|
"their expires_at property."))
|
||||||
@ -201,11 +207,14 @@ class RequestDeserializer(wsgi.JSONRequestDeserializer):
|
|||||||
msg = _("Task '%s' is required") % param
|
msg = _("Task '%s' is required") % param
|
||||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||||
|
|
||||||
def __init__(self, schema=None):
|
def __init__(self, schema=None, policy_engine=None):
|
||||||
super(RequestDeserializer, self).__init__()
|
super(RequestDeserializer, self).__init__()
|
||||||
self.schema = schema or get_task_schema()
|
self.schema = schema or get_task_schema()
|
||||||
|
# want to enforce the access policy as early as possible
|
||||||
|
self.policy_engine = policy_engine or policy.Enforcer()
|
||||||
|
|
||||||
def create(self, request):
|
def create(self, request):
|
||||||
|
_enforce_access_policy(self.policy_engine, request)
|
||||||
body = self._get_request_body(request)
|
body = self._get_request_body(request)
|
||||||
self._validate_create_body(body)
|
self._validate_create_body(body)
|
||||||
try:
|
try:
|
||||||
@ -222,6 +231,7 @@ class RequestDeserializer(wsgi.JSONRequestDeserializer):
|
|||||||
return dict(task=task)
|
return dict(task=task)
|
||||||
|
|
||||||
def index(self, request):
|
def index(self, request):
|
||||||
|
_enforce_access_policy(self.policy_engine, request)
|
||||||
params = request.params.copy()
|
params = request.params.copy()
|
||||||
limit = params.pop('limit', None)
|
limit = params.pop('limit', None)
|
||||||
marker = params.pop('marker', None)
|
marker = params.pop('marker', None)
|
||||||
@ -334,6 +344,7 @@ _TASK_SCHEMA = {
|
|||||||
"description": _("The type of task represented by this content"),
|
"description": _("The type of task represented by this content"),
|
||||||
"enum": [
|
"enum": [
|
||||||
"import",
|
"import",
|
||||||
|
"api_image_import"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@ -388,6 +399,14 @@ _TASK_SCHEMA = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _enforce_access_policy(policy_engine, request):
|
||||||
|
try:
|
||||||
|
policy_engine.enforce(request.context, 'tasks_api_access', {})
|
||||||
|
except exception.Forbidden:
|
||||||
|
LOG.debug("User does not have permission to access the Tasks API")
|
||||||
|
raise webob.exc.HTTPForbidden()
|
||||||
|
|
||||||
|
|
||||||
def get_task_schema():
|
def get_task_schema():
|
||||||
properties = copy.deepcopy(_TASK_SCHEMA)
|
properties = copy.deepcopy(_TASK_SCHEMA)
|
||||||
schema = glance.schema.Schema('task', properties)
|
schema = glance.schema.Schema('task', properties)
|
||||||
|
@ -475,6 +475,14 @@ class TestTasksControllerPolicies(base.IsolatedUnitTest):
|
|||||||
self.assertRaises(webob.exc.HTTPForbidden, self.controller.get,
|
self.assertRaises(webob.exc.HTTPForbidden, self.controller.get,
|
||||||
request, task_id=UUID2)
|
request, task_id=UUID2)
|
||||||
|
|
||||||
|
def test_access_get_unauthorized(self):
|
||||||
|
rules = {"tasks_api_access": False,
|
||||||
|
"get_task": True}
|
||||||
|
self.policy.set_rules(rules)
|
||||||
|
request = unit_test_utils.get_fake_request()
|
||||||
|
self.assertRaises(webob.exc.HTTPForbidden, self.controller.get,
|
||||||
|
request, task_id=UUID2)
|
||||||
|
|
||||||
def test_create_task_unauthorized(self):
|
def test_create_task_unauthorized(self):
|
||||||
rules = {"add_task": False}
|
rules = {"add_task": False}
|
||||||
self.policy.set_rules(rules)
|
self.policy.set_rules(rules)
|
||||||
@ -490,6 +498,72 @@ class TestTasksControllerPolicies(base.IsolatedUnitTest):
|
|||||||
request,
|
request,
|
||||||
'fake_id')
|
'fake_id')
|
||||||
|
|
||||||
|
def test_access_delete_unauthorized(self):
|
||||||
|
rules = {"tasks_api_access": False}
|
||||||
|
self.policy.set_rules(rules)
|
||||||
|
request = unit_test_utils.get_fake_request()
|
||||||
|
self.assertRaises(webob.exc.HTTPForbidden,
|
||||||
|
self.controller.delete,
|
||||||
|
request,
|
||||||
|
'fake_id')
|
||||||
|
|
||||||
|
|
||||||
|
class TestTasksDeserializerPolicies(test_utils.BaseTestCase):
|
||||||
|
|
||||||
|
# NOTE(rosmaita): this is a bit weird, but we check the access
|
||||||
|
# policy in the RequestDeserializer for calls that take bodies
|
||||||
|
# or query strings because we want to make sure the failure is
|
||||||
|
# a 403, not a 400 due to bad request format
|
||||||
|
def setUp(self):
|
||||||
|
super(TestTasksDeserializerPolicies, self).setUp()
|
||||||
|
self.policy = unit_test_utils.FakePolicyEnforcer()
|
||||||
|
self.deserializer = glance.api.v2.tasks.RequestDeserializer(
|
||||||
|
schema=None, policy_engine=self.policy)
|
||||||
|
|
||||||
|
bad_path = '/tasks?limit=NaN'
|
||||||
|
|
||||||
|
def test_access_index_authorized_bad_query_string(self):
|
||||||
|
"""Allow access, fail with 400"""
|
||||||
|
rules = {"tasks_api_access": True,
|
||||||
|
"get_tasks": True}
|
||||||
|
self.policy.set_rules(rules)
|
||||||
|
request = unit_test_utils.get_fake_request(self.bad_path)
|
||||||
|
self.assertRaises(webob.exc.HTTPBadRequest, self.deserializer.index,
|
||||||
|
request)
|
||||||
|
|
||||||
|
def test_access_index_unauthorized(self):
|
||||||
|
"""Disallow access with bad request, fail with 403"""
|
||||||
|
rules = {"tasks_api_access": False,
|
||||||
|
"get_tasks": True}
|
||||||
|
self.policy.set_rules(rules)
|
||||||
|
request = unit_test_utils.get_fake_request(self.bad_path)
|
||||||
|
self.assertRaises(webob.exc.HTTPForbidden, self.deserializer.index,
|
||||||
|
request)
|
||||||
|
|
||||||
|
bad_task = {'typo': 'import', 'input': {"import_from": "fake"}}
|
||||||
|
|
||||||
|
def test_access_create_authorized_bad_format(self):
|
||||||
|
"""Allow access, fail with 400"""
|
||||||
|
rules = {"tasks_api_access": True,
|
||||||
|
"add_task": True}
|
||||||
|
self.policy.set_rules(rules)
|
||||||
|
request = unit_test_utils.get_fake_request()
|
||||||
|
request.body = jsonutils.dump_as_bytes(self.bad_task)
|
||||||
|
self.assertRaises(webob.exc.HTTPBadRequest,
|
||||||
|
self.deserializer.create,
|
||||||
|
request)
|
||||||
|
|
||||||
|
def test_access_create_unauthorized(self):
|
||||||
|
"""Disallow access with bad request, fail with 403"""
|
||||||
|
rules = {"tasks_api_access": False,
|
||||||
|
"add_task": True}
|
||||||
|
self.policy.set_rules(rules)
|
||||||
|
request = unit_test_utils.get_fake_request()
|
||||||
|
request.body = jsonutils.dump_as_bytes(self.bad_task)
|
||||||
|
self.assertRaises(webob.exc.HTTPForbidden,
|
||||||
|
self.deserializer.create,
|
||||||
|
request)
|
||||||
|
|
||||||
|
|
||||||
class TestTasksDeserializer(test_utils.BaseTestCase):
|
class TestTasksDeserializer(test_utils.BaseTestCase):
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user