Skip to content

Commit fb2d1b3

Browse files
authored
Merge pull request #5250 from orchestral-st2/secret_masking
Secret masking in output_schema feature is added
2 parents 79fe7d8 + 5c2fe79 commit fb2d1b3

File tree

18 files changed

+783
-33
lines changed

18 files changed

+783
-33
lines changed

CHANGELOG.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@ Added
7878

7979
Contributed by @Kami.
8080

81+
* Mask secrets in output of an action execution in the API if the action has an output schema
82+
defined and one or more output parameters are marked as secret. #5250
83+
84+
Contributed by @mahesh-orch.
85+
8186
Changed
8287
~~~~~~~
8388

contrib/runners/orquesta_runner/tests/unit/test_data_flow.py

Lines changed: 73 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,14 @@
2929

3030
tests_config.parse_args()
3131

32+
from python_runner import python_runner
3233
from tests.unit import base
3334

3435
from st2common.bootstrap import actionsregistrar
3536
from st2common.bootstrap import runnersregistrar
3637
from st2common.constants import action as ac_const
38+
from st2common.constants import secrets as secrets_const
39+
from st2common.models.api import execution as ex_api_models
3740
from st2common.models.db import liveaction as lv_db_models
3841
from st2common.persistence import execution as ex_db_access
3942
from st2common.persistence import liveaction as lv_db_access
@@ -58,6 +61,23 @@
5861
st2tests.fixturesloader.get_fixtures_packs_base_path() + "/core",
5962
]
6063

64+
TEST_1 = "xyz"
65+
TEST_2 = "床前明月光 疑是地上霜 舉頭望明月 低頭思故鄉"
66+
MOCK_PY_RESULT_1 = {
67+
"stderr": "",
68+
"stdout": "",
69+
"result": {"k2": TEST_1},
70+
"exit_code": 0,
71+
}
72+
MOCK_PY_RESULT_2 = {
73+
"stderr": "",
74+
"stdout": "",
75+
"result": {"k2": TEST_2},
76+
"exit_code": 0,
77+
}
78+
MOCK_PY_OUTPUT_1 = (ac_const.LIVEACTION_STATUS_SUCCEEDED, MOCK_PY_RESULT_1, None)
79+
MOCK_PY_OUTPUT_2 = (ac_const.LIVEACTION_STATUS_SUCCEEDED, MOCK_PY_RESULT_2, None)
80+
6181

6282
@mock.patch.object(
6383
publishers.CUDPublisher, "publish_update", mock.MagicMock(return_value=None)
@@ -172,20 +192,42 @@ def assert_data_flow(self, data):
172192
# Manually handle action execution completion.
173193
wf_svc.handle_action_execution_completion(tk3_ac_ex_db)
174194

175-
# Assert task3 succeeded and workflow is completed.
195+
# Assert task3 succeeded and workflow is still running.
176196
tk3_ex_db = wf_db_access.TaskExecution.get_by_id(tk3_ex_db.id)
177197
self.assertEqual(tk3_ex_db.status, wf_statuses.SUCCEEDED)
178198
wf_ex_db = wf_db_access.WorkflowExecution.get_by_id(wf_ex_db.id)
199+
self.assertEqual(wf_ex_db.status, wf_statuses.RUNNING)
200+
201+
# Assert task4 is already completed.
202+
query_filters = {"workflow_execution": str(wf_ex_db.id), "task_id": "task4"}
203+
tk4_ex_db = wf_db_access.TaskExecution.query(**query_filters)[0]
204+
tk4_ac_ex_db = ex_db_access.ActionExecution.query(
205+
task_execution=str(tk4_ex_db.id)
206+
)[0]
207+
tk4_lv_ac_db = lv_db_access.LiveAction.get_by_id(tk4_ac_ex_db.liveaction["id"])
208+
self.assertEqual(tk4_lv_ac_db.status, ac_const.LIVEACTION_STATUS_SUCCEEDED)
209+
210+
# Manually handle action execution completion.
211+
wf_svc.handle_action_execution_completion(tk4_ac_ex_db)
212+
213+
# Assert task4 succeeded and workflow is completed.
214+
tk4_ex_db = wf_db_access.TaskExecution.get_by_id(tk4_ex_db.id)
215+
self.assertEqual(tk4_ex_db.status, wf_statuses.SUCCEEDED)
216+
wf_ex_db = wf_db_access.WorkflowExecution.get_by_id(wf_ex_db.id)
179217
self.assertEqual(wf_ex_db.status, wf_statuses.SUCCEEDED)
180218
lv_ac_db = lv_db_access.LiveAction.get_by_id(str(lv_ac_db.id))
181219
self.assertEqual(lv_ac_db.status, ac_const.LIVEACTION_STATUS_SUCCEEDED)
182220
ac_ex_db = ex_db_access.ActionExecution.get_by_id(str(ac_ex_db.id))
183221
self.assertEqual(ac_ex_db.status, ac_const.LIVEACTION_STATUS_SUCCEEDED)
184222

185223
# Check workflow output.
224+
expected_value = wf_input["a1"] if six.PY3 else wf_input["a1"].decode("utf-8")
225+
186226
expected_output = {
187-
"a5": wf_input["a1"] if six.PY3 else wf_input["a1"].decode("utf-8"),
188-
"b5": wf_input["a1"] if six.PY3 else wf_input["a1"].decode("utf-8"),
227+
"a6": expected_value,
228+
"b6": expected_value,
229+
"a7": expected_value,
230+
"b7": expected_value,
189231
}
190232

191233
self.assertDictEqual(wf_ex_db.output, expected_output)
@@ -196,8 +238,34 @@ def assert_data_flow(self, data):
196238
self.assertDictEqual(lv_ac_db.result, expected_result)
197239
self.assertDictEqual(ac_ex_db.result, expected_result)
198240

241+
# Assert expected output on conversion to API model
242+
ac_ex_api = ex_api_models.ActionExecutionAPI.from_model(
243+
ac_ex_db, mask_secrets=True
244+
)
245+
246+
expected_masked_output = {
247+
"a6": expected_value,
248+
"b6": expected_value,
249+
"a7": expected_value,
250+
"b7": secrets_const.MASKED_ATTRIBUTE_VALUE,
251+
}
252+
253+
expected_masked_result = {"output": expected_masked_output}
254+
255+
self.assertDictEqual(ac_ex_api.result, expected_masked_result)
256+
257+
@mock.patch.object(
258+
python_runner.PythonRunner,
259+
"run",
260+
mock.MagicMock(return_value=MOCK_PY_OUTPUT_1),
261+
)
199262
def test_string(self):
200-
self.assert_data_flow("xyz")
263+
self.assert_data_flow(TEST_1)
201264

265+
@mock.patch.object(
266+
python_runner.PythonRunner,
267+
"run",
268+
mock.MagicMock(return_value=MOCK_PY_OUTPUT_2),
269+
)
202270
def test_unicode_string(self):
203-
self.assert_data_flow("床前明月光 疑是地上霜 舉頭望明月 低頭思故鄉")
271+
self.assert_data_flow(TEST_2)
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
# Copyright 2021 The StackStorm Authors.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from __future__ import absolute_import
16+
17+
import mock
18+
19+
from http_runner import http_runner
20+
from python_runner import python_runner
21+
from orquesta_runner import orquesta_runner
22+
23+
import st2tests
24+
25+
import st2tests.config as tests_config
26+
27+
tests_config.parse_args()
28+
29+
from st2common.bootstrap import actionsregistrar
30+
from st2common.bootstrap import runnersregistrar
31+
from st2common.constants import action as ac_const
32+
from st2common.constants import secrets as secrets_const
33+
from st2common.models.api import execution as ex_api_models
34+
from st2common.models.db import liveaction as lv_db_models
35+
from st2common.services import action as action_service
36+
from st2common.transport import liveaction as lv_ac_xport
37+
from st2common.transport import publishers
38+
from st2tests.mocks import liveaction as mock_lv_ac_xport
39+
40+
41+
PACKS = [
42+
st2tests.fixturesloader.get_fixtures_packs_base_path() + "/dummy_pack_1",
43+
st2tests.fixturesloader.get_fixtures_packs_base_path() + "/orquesta_tests",
44+
]
45+
46+
MOCK_PYTHON_ACTION_RESULT = {
47+
"stderr": "",
48+
"stdout": "",
49+
"result": {"k1": "foobar", "k2": "shhhh!"},
50+
"exit_code": 0,
51+
}
52+
53+
MOCK_PYTHON_RUNNER_OUTPUT = (
54+
ac_const.LIVEACTION_STATUS_SUCCEEDED,
55+
MOCK_PYTHON_ACTION_RESULT,
56+
None,
57+
)
58+
59+
MOCK_HTTP_ACTION_RESULT = {
60+
"status_code": 200,
61+
"body": {"k1": "foobar", "k2": "shhhh!"},
62+
}
63+
64+
MOCK_HTTP_RUNNER_OUTPUT = (
65+
ac_const.LIVEACTION_STATUS_SUCCEEDED,
66+
MOCK_HTTP_ACTION_RESULT,
67+
None,
68+
)
69+
70+
MOCK_ORQUESTA_ACTION_RESULT = {
71+
"errors": [],
72+
"output": {"a6": "foobar", "b6": "foobar", "a7": "foobar", "b7": "shhhh!"},
73+
}
74+
75+
MOCK_ORQUESTA_RUNNER_OUTPUT = (
76+
ac_const.LIVEACTION_STATUS_SUCCEEDED,
77+
MOCK_ORQUESTA_ACTION_RESULT,
78+
None,
79+
)
80+
81+
82+
@mock.patch.object(
83+
publishers.CUDPublisher, "publish_update", mock.MagicMock(return_value=None)
84+
)
85+
@mock.patch.object(
86+
publishers.CUDPublisher,
87+
"publish_create",
88+
mock.MagicMock(side_effect=mock_lv_ac_xport.MockLiveActionPublisher.publish_create),
89+
)
90+
@mock.patch.object(
91+
lv_ac_xport.LiveActionPublisher,
92+
"publish_state",
93+
mock.MagicMock(side_effect=mock_lv_ac_xport.MockLiveActionPublisher.publish_state),
94+
)
95+
class ActionExecutionOutputSchemaTest(st2tests.ExecutionDbTestCase):
96+
@classmethod
97+
def setUpClass(cls):
98+
super(ActionExecutionOutputSchemaTest, cls).setUpClass()
99+
100+
# Register runners.
101+
runnersregistrar.register_runners()
102+
103+
# Register test pack(s).
104+
actions_registrar = actionsregistrar.ActionsRegistrar(
105+
use_pack_cache=False, fail_on_failure=True
106+
)
107+
108+
for pack in PACKS:
109+
actions_registrar.register_from_pack(pack)
110+
111+
@mock.patch.object(
112+
python_runner.PythonRunner,
113+
"run",
114+
mock.MagicMock(return_value=MOCK_PYTHON_RUNNER_OUTPUT),
115+
)
116+
def test_python_action(self):
117+
# Execute a python action with output schema and secret
118+
lv_ac_db = lv_db_models.LiveActionDB(action="dummy_pack_1.my_py_action")
119+
lv_ac_db, ac_ex_db = action_service.request(lv_ac_db)
120+
ac_ex_db = self._wait_on_ac_ex_status(
121+
ac_ex_db, ac_const.LIVEACTION_STATUS_SUCCEEDED
122+
)
123+
124+
# Assert expected output written to the database
125+
expected_output = {"k1": "foobar", "k2": "shhhh!"}
126+
self.assertDictEqual(ac_ex_db.result["result"], expected_output)
127+
128+
# Assert expected output on conversion to API model
129+
ac_ex_api = ex_api_models.ActionExecutionAPI.from_model(
130+
ac_ex_db, mask_secrets=True
131+
)
132+
expected_masked_output = {
133+
"k1": "foobar",
134+
"k2": secrets_const.MASKED_ATTRIBUTE_VALUE,
135+
}
136+
self.assertDictEqual(ac_ex_api.result["result"], expected_masked_output)
137+
138+
@mock.patch.object(
139+
http_runner.HttpRunner,
140+
"run",
141+
mock.MagicMock(return_value=MOCK_HTTP_RUNNER_OUTPUT),
142+
)
143+
def test_http_action(self):
144+
# Execute a http action with output schema and secret
145+
lv_ac_db = lv_db_models.LiveActionDB(action="dummy_pack_1.my_http_action")
146+
lv_ac_db, ac_ex_db = action_service.request(lv_ac_db)
147+
ac_ex_db = self._wait_on_ac_ex_status(
148+
ac_ex_db, ac_const.LIVEACTION_STATUS_SUCCEEDED
149+
)
150+
151+
# Assert expected output written to the database
152+
expected_output = {"k1": "foobar", "k2": "shhhh!"}
153+
self.assertDictEqual(ac_ex_db.result["body"], expected_output)
154+
155+
# Assert expected output on conversion to API model
156+
ac_ex_api = ex_api_models.ActionExecutionAPI.from_model(
157+
ac_ex_db, mask_secrets=True
158+
)
159+
expected_masked_output = {
160+
"k1": "foobar",
161+
"k2": secrets_const.MASKED_ATTRIBUTE_VALUE,
162+
}
163+
self.assertDictEqual(ac_ex_api.result["body"], expected_masked_output)
164+
165+
@mock.patch.object(
166+
orquesta_runner.OrquestaRunner,
167+
"run",
168+
mock.MagicMock(return_value=MOCK_ORQUESTA_RUNNER_OUTPUT),
169+
)
170+
def test_orquesta_action(self):
171+
wf_input = "foobar"
172+
173+
# Execute an orquesta action with output schema and secret
174+
lv_ac_db = lv_db_models.LiveActionDB(
175+
action="orquesta_tests.data-flow", parameters={"a1": wf_input}
176+
)
177+
lv_ac_db, ac_ex_db = action_service.request(lv_ac_db)
178+
ac_ex_db = self._wait_on_ac_ex_status(
179+
ac_ex_db, ac_const.LIVEACTION_STATUS_SUCCEEDED
180+
)
181+
182+
# Assert expected output written to the database
183+
expected_output = {
184+
"a6": wf_input,
185+
"b6": wf_input,
186+
"a7": wf_input,
187+
"b7": "shhhh!",
188+
}
189+
self.assertDictEqual(ac_ex_db.result["output"], expected_output)
190+
191+
# Assert expected output on conversion to API model
192+
ac_ex_api = ex_api_models.ActionExecutionAPI.from_model(
193+
ac_ex_db, mask_secrets=True
194+
)
195+
expected_masked_output = {
196+
"a6": wf_input,
197+
"b6": wf_input,
198+
"a7": wf_input,
199+
"b7": secrets_const.MASKED_ATTRIBUTE_VALUE,
200+
}
201+
self.assertDictEqual(ac_ex_api.result["output"], expected_masked_output)

0 commit comments

Comments
 (0)