Role based resource access control - get workflows

We already supported role based api access control, this series patches
will implement resource access control for mistral, so that
administrator could define the rules of resource accessibility, e.g.
admin user could get/delete/update the workflows of other tenants
according to the policy.

TODO:
- Implement update workflow by admin
- Implement delete workflow by admin
- Implement for other resources(workfbook/execution/task/action, etc.)

Partially implements: blueprint mistral-rbac

Change-Id: I8b00e8a260a74457ad037ee7322a7cba9ae34fab
This commit is contained in:
Lingxian Kong 2016-12-21 13:28:45 +13:00
parent ba2f25c0e4
commit 965db538aa
8 changed files with 115 additions and 8 deletions

View File

@ -54,6 +54,7 @@
"workflows:delete": "rule:admin_or_owner",
"workflows:get": "rule:admin_or_owner",
"workflows:list": "rule:admin_or_owner",
"workflows:list:all_projects": "rule:admin_only",
"workflows:update": "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,
wtypes.text, wtypes.text, wtypes.text, 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',
sort_dirs='asc', fields='', name=None, input=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.
:param marker: Optional. Pagination marker for large data sets.
@ -203,9 +204,13 @@ class WorkflowsController(rest.RestController, hooks.HookController):
time and date.
:param updated_at: Optional. Keep only resources with specific latest
update time and date.
:param all_projects: Optional. Get resources of all projects.
"""
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(
created_at=created_at,
name=name,
@ -218,8 +223,9 @@ class WorkflowsController(rest.RestController, hooks.HookController):
)
LOG.info("Fetch workflows. marker=%s, limit=%s, sort_keys=%s, "
"sort_dirs=%s, fields=%s, filters=%s", marker, limit,
sort_keys, sort_dirs, fields, filters)
"sort_dirs=%s, fields=%s, filters=%s, all_projects=%s",
marker, limit, sort_keys, sort_dirs, fields, filters,
all_projects)
return rest_utils.get_all(
resources.Workflows,
@ -231,5 +237,6 @@ class WorkflowsController(rest.RestController, hooks.HookController):
sort_keys=sort_keys,
sort_dirs=sort_dirs,
fields=fields,
all_projects=all_projects,
**filters
)

View File

@ -124,6 +124,9 @@ def context_from_headers_and_env(headers, env):
service_catalog = (params['service_catalog'] if is_target
else token_info.get('token', {}))
roles = headers.get('X-Roles', "").split(",")
is_admin = True if 'admin' in roles else False
return MistralContext(
auth_uri=auth_uri,
auth_cacert=auth_cacert,
@ -135,9 +138,10 @@ def context_from_headers_and_env(headers, env):
user_name=user_name,
region_name=region_name,
project_name=headers.get('X-Project-Name'),
roles=headers.get('X-Roles', "").split(","),
roles=roles,
is_trust_scoped=False,
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 datetime
import mock
from mistral.db.v2 import api as db_api
from mistral.db.v2.sqlalchemy import models
from mistral import exceptions as exc
from mistral.tests.unit.api import base
from mistral.tests.unit import base as unit_base
from mistral import utils
WF_DEFINITION = """
@ -420,6 +422,26 @@ class TestWorkflowsController(base.APITest):
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)
def test_get_all_pagination(self):
resp = self.app.get(

View File

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

View File

@ -25,6 +25,7 @@ import six
import webob
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 import exceptions as exc
@ -125,7 +126,8 @@ def filters_to_dict(**kwargs):
def get_all(list_cls, cls, get_all_function, get_function,
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.
: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
constructing 'next' link.
: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:
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_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
if marker:
@ -171,6 +181,7 @@ def get_all(list_cls, cls, get_all_function, get_function,
marker=marker_obj,
sort_keys=sort_keys,
sort_dirs=sort_dirs,
insecure=insecure,
**filters
)
@ -196,6 +207,7 @@ def get_all(list_cls, cls, get_all_function, get_function,
sort_keys=sort_keys,
sort_dirs=sort_dirs,
fields=fields,
insecure=insecure,
**filters
)

View File

@ -11,6 +11,7 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import json
from oslo_concurrency.fixture import lockutils
from tempest.lib import exceptions
@ -42,6 +43,62 @@ class WorkflowTestsV2(base.TestCase):
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')
def test_get_list_workflows_with_fields(self):
resp, body = self.client.get_list_obj('workflows?fields=name')

View File

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