Merge "Role based resource access control - get workflows"

This commit is contained in:
Jenkins 2017-01-10 11:54:03 +00:00 committed by Gerrit Code Review
commit 47a7274c76
8 changed files with 115 additions and 8 deletions

View File

@ -54,6 +54,7 @@
"workflows:delete": "rule:admin_or_owner", "workflows:delete": "rule:admin_or_owner",
"workflows:get": "rule:admin_or_owner", "workflows:get": "rule:admin_or_owner",
"workflows:list": "rule:admin_or_owner", "workflows:list": "rule:admin_or_owner",
"workflows:list:all_projects": "rule:admin_only",
"workflows:update": "rule:admin_or_owner", "workflows:update": "rule:admin_or_owner",
"event_triggers:create": "rule:admin_or_owner", "event_triggers:create": "rule:admin_or_owner",

View File

@ -171,11 +171,12 @@ class WorkflowsController(rest.RestController, hooks.HookController):
types.uniquelist, types.list, types.uniquelist, types.uniquelist, types.list, types.uniquelist,
wtypes.text, wtypes.text, wtypes.text, wtypes.text, wtypes.text, wtypes.text, wtypes.text, wtypes.text,
resources.SCOPE_TYPES, types.uuid, wtypes.text, resources.SCOPE_TYPES, types.uuid, wtypes.text,
wtypes.text) wtypes.text, bool)
def get_all(self, marker=None, limit=None, sort_keys='created_at', def get_all(self, marker=None, limit=None, sort_keys='created_at',
sort_dirs='asc', fields='', name=None, input=None, sort_dirs='asc', fields='', name=None, input=None,
definition=None, tags=None, scope=None, definition=None, tags=None, scope=None,
project_id=None, created_at=None, updated_at=None): project_id=None, created_at=None, updated_at=None,
all_projects=False):
"""Return a list of workflows. """Return a list of workflows.
:param marker: Optional. Pagination marker for large data sets. :param marker: Optional. Pagination marker for large data sets.
@ -203,9 +204,13 @@ class WorkflowsController(rest.RestController, hooks.HookController):
time and date. time and date.
:param updated_at: Optional. Keep only resources with specific latest :param updated_at: Optional. Keep only resources with specific latest
update time and date. update time and date.
:param all_projects: Optional. Get resources of all projects.
""" """
acl.enforce('workflows:list', context.ctx()) acl.enforce('workflows:list', context.ctx())
if all_projects:
acl.enforce('workflows:list:all_projects', context.ctx())
filters = filter_utils.create_filters_from_request_params( filters = filter_utils.create_filters_from_request_params(
created_at=created_at, created_at=created_at,
name=name, name=name,
@ -218,8 +223,9 @@ class WorkflowsController(rest.RestController, hooks.HookController):
) )
LOG.info("Fetch workflows. marker=%s, limit=%s, sort_keys=%s, " LOG.info("Fetch workflows. marker=%s, limit=%s, sort_keys=%s, "
"sort_dirs=%s, fields=%s, filters=%s", marker, limit, "sort_dirs=%s, fields=%s, filters=%s, all_projects=%s",
sort_keys, sort_dirs, fields, filters) marker, limit, sort_keys, sort_dirs, fields, filters,
all_projects)
return rest_utils.get_all( return rest_utils.get_all(
resources.Workflows, resources.Workflows,
@ -231,5 +237,6 @@ class WorkflowsController(rest.RestController, hooks.HookController):
sort_keys=sort_keys, sort_keys=sort_keys,
sort_dirs=sort_dirs, sort_dirs=sort_dirs,
fields=fields, fields=fields,
all_projects=all_projects,
**filters **filters
) )

View File

@ -124,6 +124,9 @@ def context_from_headers_and_env(headers, env):
service_catalog = (params['service_catalog'] if is_target service_catalog = (params['service_catalog'] if is_target
else token_info.get('token', {})) else token_info.get('token', {}))
roles = headers.get('X-Roles', "").split(",")
is_admin = True if 'admin' in roles else False
return MistralContext( return MistralContext(
auth_uri=auth_uri, auth_uri=auth_uri,
auth_cacert=auth_cacert, auth_cacert=auth_cacert,
@ -135,9 +138,10 @@ def context_from_headers_and_env(headers, env):
user_name=user_name, user_name=user_name,
region_name=region_name, region_name=region_name,
project_name=headers.get('X-Project-Name'), project_name=headers.get('X-Project-Name'),
roles=headers.get('X-Roles', "").split(","), roles=roles,
is_trust_scoped=False, is_trust_scoped=False,
expires_at=token_info['token']['expires_at'] if token_info else None, expires_at=token_info['token']['expires_at'] if token_info else None,
is_admin=is_admin
) )

View File

@ -15,12 +15,14 @@
import copy import copy
import datetime import datetime
import mock import mock
from mistral.db.v2 import api as db_api from mistral.db.v2 import api as db_api
from mistral.db.v2.sqlalchemy import models from mistral.db.v2.sqlalchemy import models
from mistral import exceptions as exc from mistral import exceptions as exc
from mistral.tests.unit.api import base from mistral.tests.unit.api import base
from mistral.tests.unit import base as unit_base
from mistral import utils from mistral import utils
WF_DEFINITION = """ WF_DEFINITION = """
@ -420,6 +422,26 @@ class TestWorkflowsController(base.APITest):
self.assertEqual(0, len(resp.json['workflows'])) self.assertEqual(0, len(resp.json['workflows']))
@mock.patch('mistral.db.v2.api.get_workflow_definitions')
@mock.patch('mistral.context.context_from_headers_and_env')
def test_get_all_projects_admin(self, mock_context, mock_get_wf_defs):
admin_ctx = unit_base.get_context(admin=True)
mock_context.return_value = admin_ctx
resp = self.app.get('/v2/workflows?all_projects=true')
self.assertEqual(200, resp.status_int)
self.assertTrue(mock_get_wf_defs.call_args[1].get('insecure', False))
def test_get_all_projects_normal_user(self):
resp = self.app.get(
'/v2/workflows?all_projects=true',
expect_errors=True
)
self.assertEqual(403, resp.status_int)
@mock.patch.object(db_api, "get_workflow_definitions", MOCK_WFS) @mock.patch.object(db_api, "get_workflow_definitions", MOCK_WFS)
def test_get_all_pagination(self): def test_get_all_pagination(self):
resp = self.app.get( resp = self.app.get(

View File

@ -68,5 +68,6 @@ policy_data = """{
"workflows:delete": "rule:admin_or_owner", "workflows:delete": "rule:admin_or_owner",
"workflows:get": "rule:admin_or_owner", "workflows:get": "rule:admin_or_owner",
"workflows:list": "rule:admin_or_owner", "workflows:list": "rule:admin_or_owner",
"workflows:list:all_projects": "rule:admin_only",
"workflows:update": "rule:admin_or_owner", "workflows:update": "rule:admin_or_owner",
}""" }"""

View File

@ -25,6 +25,7 @@ import six
import webob import webob
from wsme import exc as wsme_exc from wsme import exc as wsme_exc
from mistral import context as auth_ctx
from mistral.db.v2.sqlalchemy import api as db_api from mistral.db.v2.sqlalchemy import api as db_api
from mistral import exceptions as exc from mistral import exceptions as exc
@ -125,7 +126,8 @@ def filters_to_dict(**kwargs):
def get_all(list_cls, cls, get_all_function, get_function, def get_all(list_cls, cls, get_all_function, get_function,
resource_function=None, marker=None, limit=None, resource_function=None, marker=None, limit=None,
sort_keys='created_at', sort_dirs='asc', fields='', **filters): sort_keys='created_at', sort_dirs='asc', fields='',
all_projects=False, **filters):
"""Return a list of cls. """Return a list of cls.
:param list_cls: Collection class (e.g.: Actions, Workflows, ...). :param list_cls: Collection class (e.g.: Actions, Workflows, ...).
@ -149,6 +151,7 @@ def get_all(list_cls, cls, get_all_function, get_function,
fields if it's provided, since it will be used when fields if it's provided, since it will be used when
constructing 'next' link. constructing 'next' link.
:param filters: Optional. A specified dictionary of filters to match. :param filters: Optional. A specified dictionary of filters to match.
:param all_projects: Optional. Get resources of all projects.
""" """
if fields and 'id' not in fields: if fields and 'id' not in fields:
fields.insert(0, 'id') fields.insert(0, 'id')
@ -156,6 +159,13 @@ def get_all(list_cls, cls, get_all_function, get_function,
validate_query_params(limit, sort_keys, sort_dirs) validate_query_params(limit, sort_keys, sort_dirs)
validate_fields(fields, cls.get_fields()) validate_fields(fields, cls.get_fields())
# Admin user can get all tenants resources, no matter they are private or
# public.
insecure = False
if (all_projects or
(auth_ctx.ctx().is_admin and filters.get('project_id', ''))):
insecure = True
marker_obj = None marker_obj = None
if marker: if marker:
@ -171,6 +181,7 @@ def get_all(list_cls, cls, get_all_function, get_function,
marker=marker_obj, marker=marker_obj,
sort_keys=sort_keys, sort_keys=sort_keys,
sort_dirs=sort_dirs, sort_dirs=sort_dirs,
insecure=insecure,
**filters **filters
) )
@ -196,6 +207,7 @@ def get_all(list_cls, cls, get_all_function, get_function,
sort_keys=sort_keys, sort_keys=sort_keys,
sort_dirs=sort_dirs, sort_dirs=sort_dirs,
fields=fields, fields=fields,
insecure=insecure,
**filters **filters
) )

View File

@ -11,6 +11,7 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import json
from oslo_concurrency.fixture import lockutils from oslo_concurrency.fixture import lockutils
from tempest.lib import exceptions from tempest.lib import exceptions
@ -42,6 +43,62 @@ class WorkflowTestsV2(base.TestCase):
self.assertNotIn('next', body) self.assertNotIn('next', body)
@test.attr(type='smoke')
def test_get_list_workflows_by_admin(self):
self.useFixture(lockutils.LockFixture('mistral-workflow'))
_, body = self.client.create_workflow('wf_v2.yaml')
name = body['workflows'][0]['name']
resp, raw_body = self.admin_client.get('workflows?all_projects=true')
body = json.loads(raw_body)
self.assertEqual(200, resp.status)
names = [wf['name'] for wf in body['workflows']]
self.assertIn(name, names)
@test.attr(type='smoke')
def test_get_list_workflows_with_project_by_admin(self):
self.useFixture(lockutils.LockFixture('mistral-workflow'))
_, body = self.client.create_workflow('wf_v2.yaml')
name = body['workflows'][0]['name']
resp, raw_body = self.admin_client.get(
'workflows?project_id=%s' %
self.client.auth_provider.credentials.tenant_id
)
body = json.loads(raw_body)
self.assertEqual(200, resp.status)
names = [wf['name'] for wf in body['workflows']]
self.assertIn(name, names)
@test.attr(type='smoke')
def test_get_list_other_project_private_workflows(self):
self.useFixture(lockutils.LockFixture('mistral-workflow'))
_, body = self.client.create_workflow('wf_v2.yaml')
name = body['workflows'][0]['name']
resp, raw_body = self.alt_client.get(
'workflows?project_id=%s' %
self.client.auth_provider.credentials.tenant_id
)
body = json.loads(raw_body)
self.assertEqual(200, resp.status)
names = [wf['name'] for wf in body['workflows']]
self.assertNotIn(name, names)
@test.attr(type='smoke') @test.attr(type='smoke')
def test_get_list_workflows_with_fields(self): def test_get_list_workflows_with_fields(self):
resp, body = self.client.get_list_obj('workflows?fields=name') resp, body = self.client.get_list_obj('workflows?fields=name')

View File

@ -25,7 +25,7 @@ CONF = config.CONF
class TestCase(test.BaseTestCase): class TestCase(test.BaseTestCase):
credentials = ['primary', 'alt'] credentials = ['admin', 'primary', 'alt']
@classmethod @classmethod
def skip_checks(cls): def skip_checks(cls):
@ -46,12 +46,15 @@ class TestCase(test.BaseTestCase):
if 'WITHOUT_AUTH' in os.environ: if 'WITHOUT_AUTH' in os.environ:
cls.mgr = mock.MagicMock() cls.mgr = mock.MagicMock()
cls.mgr.auth_provider = service_base.AuthProv() cls.mgr.auth_provider = service_base.AuthProv()
cls.alt_mgr = cls.mgr cls.admin_mgr = cls.alt_mgr = cls.mgr
else: else:
cls.admin_mgr = cls.admin_manager
cls.mgr = cls.manager cls.mgr = cls.manager
cls.alt_mgr = cls.alt_manager cls.alt_mgr = cls.alt_manager
if cls._service == 'workflowv2': if cls._service == 'workflowv2':
cls.admin_client = mistral_client.MistralClientV2(
cls.admin_mgr.auth_provider, cls._service)
cls.client = mistral_client.MistralClientV2( cls.client = mistral_client.MistralClientV2(
cls.mgr.auth_provider, cls._service) cls.mgr.auth_provider, cls._service)
cls.alt_client = mistral_client.MistralClientV2( cls.alt_client = mistral_client.MistralClientV2(