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:
Brian Rosmaita 2017-08-17 18:21:25 -04:00
parent 3a281182c6
commit f6d384f184
3 changed files with 99 additions and 5 deletions

View File

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

View File

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

View File

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