From 2ba3df9e1c82330138c47e00a02ec9a7b12129da Mon Sep 17 00:00:00 2001 From: Lingxian Kong Date: Wed, 29 Mar 2017 16:29:21 +1300 Subject: [PATCH] Role based resource access control - update executions 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. This patch supports admin user to update executions of other tenants. Partially implements: blueprint mistral-rbac Change-Id: Id8445d28dcc8adfa12588ec59a4b143bd018899b --- mistral/db/v2/sqlalchemy/api.py | 17 ++- mistral/tests/unit/db/v2/test_locking.py | 4 + .../unit/db/v2/test_sqlalchemy_db_api.py | 133 +++++++++++------- 3 files changed, 104 insertions(+), 50 deletions(-) diff --git a/mistral/db/v2/sqlalchemy/api.py b/mistral/db/v2/sqlalchemy/api.py index a752e7a6..33bfa60d 100644 --- a/mistral/db/v2/sqlalchemy/api.py +++ b/mistral/db/v2/sqlalchemy/api.py @@ -28,6 +28,7 @@ import sqlalchemy as sa from sqlalchemy.ext.compiler import compiles from sqlalchemy.sql.expression import Insert +from mistral import context from mistral.db.sqlalchemy import base as b from mistral.db.sqlalchemy import model_base as mb from mistral.db.sqlalchemy import sqlite_lock @@ -255,8 +256,10 @@ def _get_db_object_by_name(model, name): return _secure_query(model).filter_by(name=name).first() -def _get_db_object_by_id(model, id): - return _secure_query(model).filter_by(id=id).first() +def _get_db_object_by_id(model, id, insecure=False): + query = b.model_query(model) if insecure else _secure_query(model) + + return query.filter_by(id=id).first() def _get_db_object_by_name_or_id(model, identifier, insecure=False): @@ -735,7 +738,13 @@ def _get_action_executions(**kwargs): @b.session_aware() def get_workflow_execution(id, session=None): - wf_ex = _get_db_object_by_id(models.WorkflowExecution, id) + ctx = context.ctx() + + wf_ex = _get_db_object_by_id( + models.WorkflowExecution, + id, + insecure=ctx.is_admin + ) if not wf_ex: raise exc.DBEntityNotFoundError( @@ -783,6 +792,8 @@ def create_workflow_execution(values, session=None): def update_workflow_execution(id, values, session=None): wf_ex = get_workflow_execution(id) + m_dbutils.check_db_obj_access(wf_ex) + wf_ex.update(values.copy()) return wf_ex diff --git a/mistral/tests/unit/db/v2/test_locking.py b/mistral/tests/unit/db/v2/test_locking.py index 2c523967..484478a2 100644 --- a/mistral/tests/unit/db/v2/test_locking.py +++ b/mistral/tests/unit/db/v2/test_locking.py @@ -19,6 +19,7 @@ from oslo_config import cfg import random import testtools +from mistral import context as auth_context from mistral.db.sqlalchemy import sqlite_lock from mistral.db.v2.sqlalchemy import api as db_api from mistral.db.v2.sqlalchemy import models as db_models @@ -89,6 +90,9 @@ class SQLiteLocksTest(test_base.DbTestCase): self.assertEqual(0, len(sqlite_lock.get_locks())) def _run_correct_locking(self, wf_ex): + # Set context info for the thread. + auth_context.set_ctx(test_base.get_context()) + self._random_sleep() with db_api.transaction(): diff --git a/mistral/tests/unit/db/v2/test_sqlalchemy_db_api.py b/mistral/tests/unit/db/v2/test_sqlalchemy_db_api.py index b76ba9cf..984978c0 100644 --- a/mistral/tests/unit/db/v2/test_sqlalchemy_db_api.py +++ b/mistral/tests/unit/db/v2/test_sqlalchemy_db_api.py @@ -29,7 +29,9 @@ from mistral.tests.unit import base as test_base from mistral.utils import filter_utils -user_context = test_base.get_context(default=False) +DEFAULT_CTX = test_base.get_context() +USER_CTX = test_base.get_context(default=False) +ADM_CTX = test_base.get_context(default=False, admin=True) WORKBOOKS = [ { @@ -305,7 +307,7 @@ class WorkbookTest(SQLAlchemyTest): self.assertEqual(created, fetched[0]) # Create a new user. - auth_context.set_ctx(test_base.get_context(default=False)) + auth_context.set_ctx(USER_CTX) created = db_api.create_workbook(WORKBOOKS[1]) fetched = db_api.get_workbooks() @@ -324,7 +326,7 @@ class WorkbookTest(SQLAlchemyTest): self.assertEqual(created1, fetched[0]) # Create a new user. - auth_context.set_ctx(test_base.get_context(default=False)) + auth_context.set_ctx(USER_CTX) fetched = db_api.get_workbooks() @@ -347,7 +349,7 @@ class WorkbookTest(SQLAlchemyTest): auth_context.ctx().project_id) # Create a new user. - auth_context.set_ctx(test_base.get_context(default=False)) + auth_context.set_ctx(USER_CTX) fetched = db_api.get_workbooks() @@ -606,7 +608,7 @@ class WorkflowDefinitionTest(SQLAlchemyTest): created = db_api.create_workflow_definition(WF_DEFINITIONS[0]) # Switch to another project. - auth_context.set_ctx(test_base.get_context(default=False)) + auth_context.set_ctx(USER_CTX) self.assertRaises( exc.NotAllowedException, @@ -619,7 +621,7 @@ class WorkflowDefinitionTest(SQLAlchemyTest): created = db_api.create_workflow_definition(WF_DEFINITIONS[1]) # Switch to admin. - auth_context.set_ctx(test_base.get_context(default=False, admin=True)) + auth_context.set_ctx(ADM_CTX) updated = db_api.update_workflow_definition( created['id'], @@ -632,7 +634,7 @@ class WorkflowDefinitionTest(SQLAlchemyTest): self.assertEqual('my new definition', updated.definition) # Switch back. - auth_context.set_ctx(test_base.get_context()) + auth_context.set_ctx(DEFAULT_CTX) fetched = db_api.get_workflow_definition(created['id']) @@ -645,7 +647,7 @@ class WorkflowDefinitionTest(SQLAlchemyTest): created = db_api.create_workflow_definition(system_workflow) # Switch to admin. - auth_context.set_ctx(test_base.get_context(default=False, admin=True)) + auth_context.set_ctx(ADM_CTX) updated = db_api.update_workflow_definition( created['id'], @@ -689,13 +691,13 @@ class WorkflowDefinitionTest(SQLAlchemyTest): created = db_api.create_workflow_definition(WF_DEFINITIONS[0]) # Create a new user. - auth_context.set_ctx(test_base.get_context(default=False)) + auth_context.set_ctx(USER_CTX) cron_trigger = copy.copy(CRON_TRIGGER) cron_trigger['workflow_id'] = created.id db_api.create_cron_trigger(cron_trigger) - auth_context.set_ctx(test_base.get_context(default=True)) + auth_context.set_ctx(DEFAULT_CTX) self.assertRaises( exc.NotAllowedException, @@ -708,7 +710,7 @@ class WorkflowDefinitionTest(SQLAlchemyTest): created = db_api.create_workflow_definition(WF_DEFINITIONS[0]) # Switch to another user. - auth_context.set_ctx(test_base.get_context(default=False)) + auth_context.set_ctx(USER_CTX) event_trigger = copy.copy(EVENT_TRIGGERS[0]) event_trigger.update({'workflow_id': created.id}) @@ -716,7 +718,7 @@ class WorkflowDefinitionTest(SQLAlchemyTest): db_api.create_event_trigger(event_trigger) # Switch back. - auth_context.set_ctx(test_base.get_context(default=True)) + auth_context.set_ctx(DEFAULT_CTX) self.assertRaises( exc.NotAllowedException, @@ -810,7 +812,7 @@ class WorkflowDefinitionTest(SQLAlchemyTest): created = db_api.create_workflow_definition(WF_DEFINITIONS[0]) # Switch to another project. - auth_context.set_ctx(test_base.get_context(default=False)) + auth_context.set_ctx(USER_CTX) self.assertRaises( exc.NotAllowedException, @@ -822,12 +824,12 @@ class WorkflowDefinitionTest(SQLAlchemyTest): created = db_api.create_workflow_definition(WF_DEFINITIONS[0]) # Switch to admin. - auth_context.set_ctx(test_base.get_context(default=False, admin=True)) + auth_context.set_ctx(ADM_CTX) db_api.delete_workflow_definition(created['id']) # Switch back. - auth_context.set_ctx(test_base.get_context()) + auth_context.set_ctx(DEFAULT_CTX) self.assertRaises( exc.DBEntityNotFoundError, @@ -846,7 +848,7 @@ class WorkflowDefinitionTest(SQLAlchemyTest): self.assertEqual(created1, fetched[0]) # Create a new user. - auth_context.set_ctx(test_base.get_context(default=False)) + auth_context.set_ctx(USER_CTX) fetched = db_api.get_workflow_definitions() @@ -871,7 +873,7 @@ class WorkflowDefinitionTest(SQLAlchemyTest): ) # Create a new user. - auth_context.set_ctx(test_base.get_context(default=False)) + auth_context.set_ctx(USER_CTX) fetched = db_api.get_workflow_definitions() @@ -1352,7 +1354,7 @@ class ActionExecutionTest(SQLAlchemyTest): created = db_api.create_action_execution(ACTION_EXECS[0]) # Create a new user. - auth_context.set_ctx(test_base.get_context(default=False)) + auth_context.set_ctx(USER_CTX) self.assertRaises( exc.DBEntityNotFoundError, @@ -1455,6 +1457,43 @@ class WorkflowExecutionTest(SQLAlchemyTest): self.assertEqual(updated, fetched) self.assertIsNotNone(fetched.updated_at) + def test_update_workflow_execution_by_admin(self): + with db_api.transaction(): + created = db_api.create_workflow_execution(WF_EXECS[0]) + + auth_context.set_ctx(ADM_CTX) + + updated = db_api.update_workflow_execution( + created.id, + {'state': 'RUNNING', 'state_info': "Running..."} + ) + + auth_context.set_ctx(DEFAULT_CTX) + + self.assertEqual('RUNNING', updated.state) + self.assertEqual( + 'RUNNING', + db_api.load_workflow_execution(updated.id).state + ) + + fetched = db_api.get_workflow_execution(created.id) + + self.assertEqual(updated, fetched) + self.assertIsNotNone(fetched.updated_at) + + def test_update_workflow_execution_by_others_fail(self): + with db_api.transaction(): + created = db_api.create_workflow_execution(WF_EXECS[0]) + + auth_context.set_ctx(USER_CTX) + + self.assertRaises( + exc.DBEntityNotFoundError, + db_api.update_workflow_execution, + created.id, + {'state': 'RUNNING', 'state_info': "Running..."} + ) + def test_create_or_update_workflow_execution(self): id = 'not-existing-id' @@ -2243,7 +2282,7 @@ class CronTriggerTest(SQLAlchemyTest): created0 = db_api.create_cron_trigger(CRON_TRIGGERS[0]) # Switch to another tenant. - auth_context.set_ctx(user_context) + auth_context.set_ctx(USER_CTX) fetched = db_api.get_cron_triggers( insecure=True, @@ -2559,7 +2598,7 @@ RESOURCE_MEMBERS = [ 'resource_id': '123e4567-e89b-12d3-a456-426655440000', 'resource_type': 'workflow', 'project_id': security.get_project_id(), - 'member_id': user_context.project_id, + 'member_id': USER_CTX.project_id, 'status': 'pending', }, { @@ -2580,18 +2619,18 @@ class ResourceMemberTest(SQLAlchemyTest): fetched = db_api.get_resource_member( '123e4567-e89b-12d3-a456-426655440000', 'workflow', - user_context.project_id + USER_CTX.project_id ) self.assertEqual(created_1, fetched) # Switch to another tenant. - auth_context.set_ctx(user_context) + auth_context.set_ctx(USER_CTX) fetched = db_api.get_resource_member( '123e4567-e89b-12d3-a456-426655440000', 'workflow', - user_context.project_id + USER_CTX.project_id ) self.assertEqual(created_1, fetched) @@ -2630,7 +2669,7 @@ class ResourceMemberTest(SQLAlchemyTest): db_api.create_resource_member(RESOURCE_MEMBERS[1]) # Switch to another tenant. - auth_context.set_ctx(user_context) + auth_context.set_ctx(USER_CTX) fetched = db_api.get_resource_members( created.resource_id, @@ -2644,12 +2683,12 @@ class ResourceMemberTest(SQLAlchemyTest): created = db_api.create_resource_member(RESOURCE_MEMBERS[0]) # Switch to another tenant. - auth_context.set_ctx(user_context) + auth_context.set_ctx(USER_CTX) updated = db_api.update_resource_member( created.resource_id, 'workflow', - user_context.project_id, + USER_CTX.project_id, {'status': 'accepted'} ) @@ -2664,7 +2703,7 @@ class ResourceMemberTest(SQLAlchemyTest): db_api.update_resource_member, created.resource_id, 'workflow', - user_context.project_id, + USER_CTX.project_id, {'status': 'accepted'} ) @@ -2674,7 +2713,7 @@ class ResourceMemberTest(SQLAlchemyTest): db_api.delete_resource_member( created.resource_id, 'workflow', - user_context.project_id, + USER_CTX.project_id, ) fetched = db_api.get_resource_members( @@ -2688,14 +2727,14 @@ class ResourceMemberTest(SQLAlchemyTest): created = db_api.create_resource_member(RESOURCE_MEMBERS[0]) # Switch to another tenant. - auth_context.set_ctx(user_context) + auth_context.set_ctx(USER_CTX) self.assertRaises( exc.DBEntityNotFoundError, db_api.delete_resource_member, created.resource_id, 'workflow', - user_context.project_id, + USER_CTX.project_id, ) def test_delete_resource_member_already_deleted(self): @@ -2704,7 +2743,7 @@ class ResourceMemberTest(SQLAlchemyTest): db_api.delete_resource_member( created.resource_id, 'workflow', - user_context.project_id, + USER_CTX.project_id, ) self.assertRaises( @@ -2712,7 +2751,7 @@ class ResourceMemberTest(SQLAlchemyTest): db_api.delete_resource_member, created.resource_id, 'workflow', - user_context.project_id, + USER_CTX.project_id, ) def test_delete_nonexistent_resource_member(self): @@ -2730,7 +2769,7 @@ class WorkflowSharingTest(SQLAlchemyTest): wf = db_api.create_workflow_definition(WF_DEFINITIONS[1]) # Switch to another tenant. - auth_context.set_ctx(user_context) + auth_context.set_ctx(USER_CTX) self.assertRaises( exc.DBEntityNotFoundError, @@ -2739,25 +2778,25 @@ class WorkflowSharingTest(SQLAlchemyTest): ) # Switch to original tenant, share workflow to another tenant. - auth_context.set_ctx(test_base.get_context()) + auth_context.set_ctx(DEFAULT_CTX) workflow_sharing = { 'resource_id': wf.id, 'resource_type': 'workflow', 'project_id': security.get_project_id(), - 'member_id': user_context.project_id, + 'member_id': USER_CTX.project_id, 'status': 'pending', } db_api.create_resource_member(workflow_sharing) # Switch to another tenant, accept the sharing, get workflows. - auth_context.set_ctx(user_context) + auth_context.set_ctx(USER_CTX) db_api.update_resource_member( wf.id, 'workflow', - user_context.project_id, + USER_CTX.project_id, {'status': 'accepted'} ) @@ -2772,19 +2811,19 @@ class WorkflowSharingTest(SQLAlchemyTest): 'resource_id': wf.id, 'resource_type': 'workflow', 'project_id': security.get_project_id(), - 'member_id': user_context.project_id, + 'member_id': USER_CTX.project_id, 'status': 'pending', } db_api.create_resource_member(workflow_sharing) # Switch to another tenant, accept the sharing. - auth_context.set_ctx(user_context) + auth_context.set_ctx(USER_CTX) db_api.update_resource_member( wf.id, 'workflow', - user_context.project_id, + USER_CTX.project_id, {'status': 'accepted'} ) @@ -2793,12 +2832,12 @@ class WorkflowSharingTest(SQLAlchemyTest): self.assertEqual(wf, fetched) # Switch to original tenant, delete the workflow. - auth_context.set_ctx(test_base.get_context()) + auth_context.set_ctx(DEFAULT_CTX) db_api.delete_workflow_definition(wf.id) # Switch to another tenant, can not see that workflow. - auth_context.set_ctx(user_context) + auth_context.set_ctx(USER_CTX) self.assertRaises( exc.DBEntityNotFoundError, @@ -2813,19 +2852,19 @@ class WorkflowSharingTest(SQLAlchemyTest): 'resource_id': wf.id, 'resource_type': 'workflow', 'project_id': security.get_project_id(), - 'member_id': user_context.project_id, + 'member_id': USER_CTX.project_id, 'status': 'pending', } db_api.create_resource_member(workflow_sharing) # Switch to another tenant, accept the sharing. - auth_context.set_ctx(user_context) + auth_context.set_ctx(USER_CTX) db_api.update_resource_member( wf.id, 'workflow', - user_context.project_id, + USER_CTX.project_id, {'status': 'accepted'} ) @@ -2834,7 +2873,7 @@ class WorkflowSharingTest(SQLAlchemyTest): db_api.create_cron_trigger(CRON_TRIGGERS[0]) # Switch to original tenant, try to delete the workflow. - auth_context.set_ctx(test_base.get_context()) + auth_context.set_ctx(DEFAULT_CTX) self.assertRaises( exc.DBError, @@ -2893,7 +2932,7 @@ class EventTriggerTest(SQLAlchemyTest): db_api.create_event_trigger(EVENT_TRIGGERS[0]) # Switch to another tenant. - auth_context.set_ctx(user_context) + auth_context.set_ctx(USER_CTX) db_api.create_event_trigger(EVENT_TRIGGERS[1]) fetched = db_api.get_event_triggers()