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:
parent
ba2f25c0e4
commit
965db538aa
@ -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",
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -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(
|
||||||
|
@ -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",
|
||||||
}"""
|
}"""
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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')
|
||||||
|
@ -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(
|
||||||
|
Loading…
Reference in New Issue
Block a user