From e780ffb06d4e2718045908982b32f3c809282cb4 Mon Sep 17 00:00:00 2001
From: Nikolay Mahotkin <nikolay.makhotkin@gmail.com>
Date: Mon, 24 Jul 2017 14:51:49 +0300
Subject: [PATCH] [Event-triggers] Allow public triggers

 * Allowed creating via API
 * Added the corresponding processing
   in the event engine
 * Corresponding test

Closes-Bug: #1704111

Change-Id: I73a1d10fe684f1ec962e42e9700064d06cb0bcbe
---
 etc/policy.json                               |  1 +
 mistral/api/controllers/v2/event_trigger.py   |  4 ++
 mistral/event_engine/default_event_engine.py  | 18 ++++-
 mistral/services/triggers.py                  |  4 +-
 .../tests/unit/api/v2/test_event_trigger.py   | 16 +++++
 .../tests/unit/services/test_event_engine.py  | 67 ++++++++++++++++++-
 6 files changed, 105 insertions(+), 5 deletions(-)

diff --git a/etc/policy.json b/etc/policy.json
index becf77c8e..6a454536b 100644
--- a/etc/policy.json
+++ b/etc/policy.json
@@ -58,6 +58,7 @@
     "workflows:update": "rule:admin_or_owner",
 
     "event_triggers:create": "rule:admin_or_owner",
+    "event_triggers:create:public": "rule:admin_only",
     "event_triggers:delete": "rule:admin_or_owner",
     "event_triggers:get": "rule:admin_or_owner",
     "event_triggers:list": "rule:admin_or_owner",
diff --git a/mistral/api/controllers/v2/event_trigger.py b/mistral/api/controllers/v2/event_trigger.py
index 05f88d2c3..71c4a118b 100644
--- a/mistral/api/controllers/v2/event_trigger.py
+++ b/mistral/api/controllers/v2/event_trigger.py
@@ -61,6 +61,9 @@ class EventTriggersController(rest.RestController):
                 CREATE_MANDATORY
             )
 
+        if values.get('scope') == 'public':
+            acl.enforce('event_triggers:create:public', auth_ctx.ctx())
+
         LOG.info('Create event trigger: %s', values)
 
         db_model = triggers.create_event_trigger(
@@ -69,6 +72,7 @@ class EventTriggersController(rest.RestController):
             values.get('topic'),
             values.get('event'),
             values.get('workflow_id'),
+            values.get('scope'),
             workflow_input=values.get('workflow_input'),
             workflow_params=values.get('workflow_params'),
         )
diff --git a/mistral/event_engine/default_event_engine.py b/mistral/event_engine/default_event_engine.py
index f0b3f9e89..ac9754b59 100644
--- a/mistral/event_engine/default_event_engine.py
+++ b/mistral/event_engine/default_event_engine.py
@@ -271,12 +271,26 @@ class DefaultEventEngine(base.EventEngine):
                     # There may be more projects registered the same event.
                     project_ids = [t['project_id'] for t in triggers]
 
+                    any_public = any(
+                        [t['scope'] == 'public' for t in triggers]
+                    )
+
                     # Skip the event doesn't belong to any event trigger owner.
-                    if (CONF.pecan.auth_enable and
+                    if (not any_public and CONF.pecan.auth_enable and
                             context.get('project_id', '') not in project_ids):
                         self.event_queue.task_done()
                         continue
 
+                    # Need to choose what trigger(s) should be called exactly.
+                    triggers_to_call = []
+                    for t in triggers:
+                        project_trigger = (
+                            t['project_id'] == context.get('project_id')
+                        )
+                        public_trigger = t['scope'] == 'public'
+                        if project_trigger or public_trigger:
+                            triggers_to_call.append(t)
+
                     LOG.debug('Start to handle event: %s, %d trigger(s) '
                               'registered.', event_type, len(triggers))
 
@@ -285,7 +299,7 @@ class DefaultEventEngine(base.EventEngine):
                         event
                     )
 
-                    self._start_workflow(triggers, event_params)
+                    self._start_workflow(triggers_to_call, event_params)
 
             self.event_queue.task_done()
 
diff --git a/mistral/services/triggers.py b/mistral/services/triggers.py
index b6497b4a7..0aac20f03 100644
--- a/mistral/services/triggers.py
+++ b/mistral/services/triggers.py
@@ -147,7 +147,8 @@ def delete_cron_trigger(name, trust_id=None):
 
 
 def create_event_trigger(name, exchange, topic, event, workflow_id,
-                         workflow_input=None, workflow_params=None):
+                         scope='private', workflow_input=None,
+                         workflow_params=None):
     with db_api.transaction():
         wf_def = db_api.get_workflow_definition_by_id(workflow_id)
 
@@ -172,6 +173,7 @@ def create_event_trigger(name, exchange, topic, event, workflow_id,
             'exchange': exchange,
             'topic': topic,
             'event': event,
+            'scope': scope,
         }
 
         security.add_trust_id(values)
diff --git a/mistral/tests/unit/api/v2/test_event_trigger.py b/mistral/tests/unit/api/v2/test_event_trigger.py
index 2ff10282e..d705c78e9 100644
--- a/mistral/tests/unit/api/v2/test_event_trigger.py
+++ b/mistral/tests/unit/api/v2/test_event_trigger.py
@@ -19,6 +19,7 @@ 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.services import triggers
 from mistral.tests.unit.api import base
 from mistral.tests.unit import base as unit_base
 
@@ -108,6 +109,21 @@ class TestEventTriggerController(base.APITest):
             client.create_event_trigger.call_args[0][1]
         )
 
+    @mock.patch.object(db_api, "get_workflow_definition_by_id", MOCK_WF)
+    @mock.patch.object(db_api, "get_workflow_definition", MOCK_WF)
+    @mock.patch.object(triggers, "create_event_trigger")
+    def test_post_public(self, create_trigger):
+        trigger = copy.deepcopy(TRIGGER)
+        trigger['scope'] = 'public'
+        trigger.pop('id')
+
+        resp = self.app.post_json('/v2/event_triggers', trigger)
+
+        self.assertEqual(201, resp.status_int)
+
+        self.assertTrue(create_trigger.called)
+        self.assertEqual('public', create_trigger.call_args[0][5])
+
     def test_post_no_workflow_id(self):
         CREATE_TRIGGER = copy.deepcopy(TRIGGER)
         CREATE_TRIGGER.pop('id')
diff --git a/mistral/tests/unit/services/test_event_engine.py b/mistral/tests/unit/services/test_event_engine.py
index 652a76119..cc2773d40 100644
--- a/mistral/tests/unit/services/test_event_engine.py
+++ b/mistral/tests/unit/services/test_event_engine.py
@@ -13,11 +13,13 @@
 #    See the License for the specific language governing permissions and
 #    limitations under the License.
 
+import copy
 import time
 
 import mock
 from oslo_config import cfg
 
+from mistral import context as auth_context
 from mistral.db.v2.sqlalchemy import api as db_api
 from mistral.event_engine import default_event_engine as evt_eng
 from mistral.rpc import clients as rpc
@@ -92,9 +94,67 @@ class EventEngineTest(base.DbTestCase):
         )
         self.assertEqual(1, len(e_engine.exchange_topic_listener_map))
 
+    @mock.patch('mistral.messaging.start_listener')
+    @mock.patch.object(rpc, 'get_engine_client', mock.Mock())
+    def test_event_engine_public_trigger(self, mock_start):
+        t = copy.deepcopy(EVENT_TRIGGER)
+
+        # Create public trigger as an admin
+        self.ctx = base.get_context(default=False, admin=True)
+        auth_context.set_ctx(self.ctx)
+
+        t['scope'] = 'public'
+        t['project_id'] = self.ctx.tenant
+        trigger = db_api.create_event_trigger(t)
+
+        # Switch to the user.
+        self.ctx = base.get_context(default=True)
+        auth_context.set_ctx(self.ctx)
+
+        e_engine = evt_eng.DefaultEventEngine()
+
+        self.addCleanup(e_engine.handler_tg.stop)
+
+        event = {
+            'event_type': EVENT_TYPE,
+            'payload': {},
+            'publisher': 'fake_publisher',
+            'timestamp': '',
+            'context': {
+                'project_id': '%s' % self.ctx.project_id,
+                'user_id': 'fake_user'
+            },
+        }
+
+        # Moreover, assert that trigger.project_id != event.project_id
+        self.assertNotEqual(
+            trigger.project_id, event['context']['project_id']
+        )
+
+        with mock.patch.object(e_engine, 'engine_client') as client_mock:
+            e_engine.event_queue.put(event)
+
+            time.sleep(1)
+
+            self.assertEqual(1, client_mock.start_workflow.call_count)
+
+            args, kwargs = client_mock.start_workflow.call_args
+
+            self.assertEqual((EVENT_TRIGGER['workflow_id'], {}), args)
+            self.assertDictEqual(
+                {
+                    'service': 'fake_publisher',
+                    'project_id': '%s' % self.ctx.project_id,
+                    'user_id': 'fake_user',
+                    'timestamp': ''
+                },
+                kwargs['event_params']
+            )
+
     @mock.patch('mistral.messaging.start_listener')
     @mock.patch.object(rpc, 'get_engine_client', mock.Mock())
     def test_process_event_queue(self, mock_start):
+        EVENT_TRIGGER['project_id'] = self.ctx.project_id
         db_api.create_event_trigger(EVENT_TRIGGER)
 
         e_engine = evt_eng.DefaultEventEngine()
@@ -106,7 +166,10 @@ class EventEngineTest(base.DbTestCase):
             'payload': {},
             'publisher': 'fake_publisher',
             'timestamp': '',
-            'context': {'project_id': 'fake_project', 'user_id': 'fake_user'},
+            'context': {
+                'project_id': '%s' % self.ctx.project_id,
+                'user_id': 'fake_user'
+            },
         }
 
         with mock.patch.object(e_engine, 'engine_client') as client_mock:
@@ -122,7 +185,7 @@ class EventEngineTest(base.DbTestCase):
             self.assertDictEqual(
                 {
                     'service': 'fake_publisher',
-                    'project_id': 'fake_project',
+                    'project_id': '%s' % self.ctx.project_id,
                     'user_id': 'fake_user',
                     'timestamp': ''
                 },