Skip to content

Commit 9abb54b

Browse files
authored
Merge pull request #5764 from StackStorm/action-execution-kvp-rbac
Apply RBAC to rendering of KVP during action execution
2 parents 6aa6198 + 97a1a40 commit 9abb54b

File tree

6 files changed

+170
-23
lines changed

6 files changed

+170
-23
lines changed

CHANGELOG.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ Fixed
3939
* Fixed generation of `st2.conf.sample` to show correct syntax for `[sensorcontainer].partition_provider` (space separated `key:value` pairs). #5710
4040
Contributed by @cognifloyd
4141

42+
* Fix access to key-value pairs in workflow and action execution where RBAC rules did not get applied
43+
44+
Contributed by @m4dcoder
45+
4246
Added
4347
~~~~~
4448

st2common/st2common/services/keyvalues.py

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,21 +15,26 @@
1515

1616
from __future__ import absolute_import
1717

18+
from oslo_config import cfg
19+
1820
from st2common import log as logging
1921

2022
from st2common.constants.keyvalue import DATASTORE_PARENT_SCOPE
2123
from st2common.constants.keyvalue import SYSTEM_SCOPE, FULL_SYSTEM_SCOPE
2224
from st2common.constants.keyvalue import USER_SCOPE, FULL_USER_SCOPE
2325
from st2common.constants.keyvalue import ALLOWED_SCOPES
2426
from st2common.constants.keyvalue import DATASTORE_KEY_SEPARATOR, USER_SEPARATOR
27+
from st2common.constants.types import ResourceType
2528
from st2common.exceptions.db import StackStormDBObjectNotFoundError
2629
from st2common.exceptions.keyvalue import InvalidScopeException, InvalidUserException
30+
from st2common.models.db.auth import UserDB
2731
from st2common.models.system.keyvalue import UserKeyReference
2832
from st2common.persistence.keyvalue import KeyValuePair
2933
from st2common.persistence.rbac import UserRoleAssignment
3034
from st2common.persistence.rbac import Role
3135
from st2common.persistence.rbac import PermissionGrant
32-
from st2common.constants.types import ResourceType
36+
from st2common.rbac.backends import get_rbac_backend
37+
from st2common.rbac.types import PermissionType
3338

3439
__all__ = [
3540
"get_kvp_for_name",
@@ -103,7 +108,12 @@ class KeyValueLookup(BaseKeyValueLookup):
103108
scope = SYSTEM_SCOPE
104109

105110
def __init__(
106-
self, prefix=None, key_prefix=None, cache=None, scope=FULL_SYSTEM_SCOPE
111+
self,
112+
prefix=None,
113+
key_prefix=None,
114+
cache=None,
115+
scope=FULL_SYSTEM_SCOPE,
116+
context=None,
107117
):
108118
if not scope:
109119
scope = FULL_SYSTEM_SCOPE
@@ -116,6 +126,18 @@ def __init__(
116126
self._value_cache = cache or {}
117127
self._scope = scope
118128

129+
self._context = context if context else dict()
130+
self._user = (
131+
context["user"]
132+
if context and "user" in context and context["user"]
133+
else cfg.CONF.system_user.user
134+
)
135+
self._user = (
136+
context["api_user"]
137+
if context and "api_user" in context and context["api_user"]
138+
else self._user
139+
)
140+
119141
def __str__(self):
120142
return self._value_cache[self._key_prefix]
121143

@@ -154,6 +176,7 @@ def _get(self, name):
154176
key_prefix=key,
155177
cache=self._value_cache,
156178
scope=self._scope,
179+
context=self._context,
157180
)
158181

159182
def _get_kv(self, key):
@@ -167,6 +190,19 @@ def _get_kv(self, key):
167190

168191
if kvp:
169192
LOG.debug("Got value %s from datastore.", kvp.value)
193+
194+
# Check that user has permission to the key value pair.
195+
# If RBAC is enabled, this check will verify if user has system role with all access.
196+
# If RBAC is enabled, this check guards against a user accessing another user's kvp.
197+
# If RBAC is enabled, user needs to be explicitly granted permission to view a system kvp.
198+
# The check is sufficient to allow decryption of the system kvp.
199+
rbac_utils = get_rbac_backend().get_utils_class()
200+
rbac_utils.assert_user_has_resource_db_permission(
201+
user_db=UserDB(name=self._user),
202+
resource_db=kvp,
203+
permission_type=PermissionType.KEY_VALUE_PAIR_VIEW,
204+
)
205+
170206
return kvp.value if kvp else ""
171207

172208

@@ -175,7 +211,13 @@ class UserKeyValueLookup(BaseKeyValueLookup):
175211
scope = USER_SCOPE
176212

177213
def __init__(
178-
self, user, prefix=None, key_prefix=None, cache=None, scope=FULL_USER_SCOPE
214+
self,
215+
user,
216+
prefix=None,
217+
key_prefix=None,
218+
cache=None,
219+
scope=FULL_USER_SCOPE,
220+
context=None,
179221
):
180222
if not scope:
181223
scope = FULL_USER_SCOPE
@@ -188,6 +230,7 @@ def __init__(
188230
self._value_cache = cache or {}
189231
self._user = user
190232
self._scope = scope
233+
self._context = context if context else dict()
191234

192235
def __str__(self):
193236
return self._value_cache[self._key_prefix]

st2common/st2common/util/keyvalue.py

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,14 @@
2121

2222
from st2common.constants.keyvalue import ALL_SCOPE, DATASTORE_PARENT_SCOPE
2323
from st2common.constants.keyvalue import DATASTORE_SCOPE_SEPARATOR
24-
from st2common.rbac.backends import get_rbac_backend
24+
from st2common.constants.keyvalue import FULL_SYSTEM_SCOPE, FULL_USER_SCOPE
25+
from st2common.constants.keyvalue import USER_SCOPE, ALLOWED_SCOPES
26+
from st2common.exceptions.rbac import AccessDeniedError
27+
from st2common.models.db.auth import UserDB
2528
from st2common.persistence.keyvalue import KeyValuePair
2629
from st2common.services.config import deserialize_key_value
27-
from st2common.constants.keyvalue import (
28-
FULL_SYSTEM_SCOPE,
29-
FULL_USER_SCOPE,
30-
USER_SCOPE,
31-
ALLOWED_SCOPES,
32-
)
33-
from st2common.models.db.auth import UserDB
34-
from st2common.exceptions.rbac import AccessDeniedError
30+
from st2common.rbac.backends import get_rbac_backend
31+
from st2common.rbac.types import PermissionType
3532

3633
__all__ = ["get_datastore_full_scope", "get_key"]
3734

@@ -106,17 +103,21 @@ def get_key(key=None, user_db=None, scope=None, decrypt=False):
106103

107104
_validate_scope(scope=scope)
108105

109-
rbac_utils = get_rbac_backend().get_utils_class()
110-
is_admin = rbac_utils.user_is_admin(user_db=user_db)
111-
112-
# User needs to be either admin or requesting item for itself
113-
_validate_decrypt_query_parameter(
114-
decrypt=decrypt, scope=scope, is_admin=is_admin, user_db=user_db
115-
)
116-
117106
# Get the key value pair by scope and name.
118107
kvp = KeyValuePair.get_by_scope_and_name(scope, key_id)
119108

109+
# Check that user has permission to the key value pair.
110+
# If RBAC is enabled, this check will verify if user has system role with all access.
111+
# If RBAC is enabled, this check guards against a user accessing another user's kvp.
112+
# If RBAC is enabled, user needs to be explicitly granted permission to view a system kvp.
113+
# The check is sufficient to allow decryption of the system kvp.
114+
rbac_utils = get_rbac_backend().get_utils_class()
115+
rbac_utils.assert_user_has_resource_db_permission(
116+
user_db=user_db,
117+
resource_db=kvp,
118+
permission_type=PermissionType.KEY_VALUE_PAIR_VIEW,
119+
)
120+
120121
# Decrypt in deserialize_key_value cannot handle NoneType.
121122
if kvp.value is None:
122123
return kvp.value

st2common/st2common/util/param.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,9 @@ def _create_graph(action_context, config):
9292
Creates a generic directed graph for depencency tree and fills it with basic context variables
9393
"""
9494
G = nx.DiGraph()
95-
system_keyvalue_context = {SYSTEM_SCOPE: KeyValueLookup(scope=FULL_SYSTEM_SCOPE)}
95+
system_keyvalue_context = {
96+
SYSTEM_SCOPE: KeyValueLookup(scope=FULL_SYSTEM_SCOPE, context=action_context)
97+
}
9698

9799
# If both 'user' and 'api_user' are specified, this prioritize 'api_user'
98100
user = action_context["user"] if "user" in action_context else None
@@ -107,7 +109,7 @@ def _create_graph(action_context, config):
107109
)
108110

109111
system_keyvalue_context[USER_SCOPE] = UserKeyValueLookup(
110-
scope=FULL_USER_SCOPE, user=user
112+
scope=FULL_USER_SCOPE, user=user, context=action_context
111113
)
112114
G.add_node(DATASTORE_PARENT_SCOPE, value=system_keyvalue_context)
113115
G.add_node(ACTION_CONTEXT_KV_PREFIX, value=action_context)

st2common/tests/unit/test_keyvalue_lookup.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,39 @@
1414
# limitations under the License.
1515

1616
from __future__ import absolute_import
17+
18+
import mock
19+
20+
from oslo_config import cfg
21+
1722
from st2tests.base import CleanDbTestCase
1823
from st2common.constants.keyvalue import FULL_SYSTEM_SCOPE, FULL_USER_SCOPE
1924
from st2common.constants.keyvalue import SYSTEM_SCOPE, USER_SCOPE
25+
from st2common.constants.types import ResourceType
26+
from st2common.exceptions.rbac import ResourceAccessDeniedError
27+
from st2common.models.db.auth import UserDB
2028
from st2common.models.db.keyvalue import KeyValuePairDB
2129
from st2common.persistence.keyvalue import KeyValuePair
30+
from st2common.rbac.backends.noop import NoOpRBACUtils
31+
from st2common.rbac.types import PermissionType
2232
from st2common.services.keyvalues import KeyValueLookup, UserKeyValueLookup
33+
from st2tests import config
34+
35+
USER = "stanley"
36+
RESOURCE_UUID = "%s:%s:%s" % (
37+
ResourceType.KEY_VALUE_PAIR,
38+
FULL_USER_SCOPE,
39+
"stanley:foobar",
40+
)
2341

2442

2543
class TestKeyValueLookup(CleanDbTestCase):
44+
@classmethod
45+
def setUpClass(cls):
46+
super(TestKeyValueLookup, cls).setUpClass()
47+
config.parse_args()
48+
cfg.CONF.set_override(name="backend", override="noop", group="rbac")
49+
2650
def test_lookup_with_key_prefix(self):
2751
KeyValuePair.add_or_update(
2852
KeyValuePairDB(
@@ -171,3 +195,27 @@ def test_lookup_cast(self):
171195
self.assertEqual(str(lookup.count), "5.5")
172196
self.assertEqual(float(lookup.count), 5.5)
173197
self.assertEqual(int(lookup.count), 5)
198+
199+
@mock.patch.object(
200+
NoOpRBACUtils,
201+
"assert_user_has_resource_db_permission",
202+
mock.MagicMock(
203+
side_effect=ResourceAccessDeniedError(
204+
user_db=UserDB(name=USER),
205+
resource_api_or_db=KeyValuePairDB(uid=RESOURCE_UUID),
206+
permission_type=PermissionType.KEY_VALUE_PAIR_VIEW,
207+
)
208+
),
209+
)
210+
def test_system_kvp_lookup_unauthorized(self):
211+
secret_value = (
212+
"0055A2D9A09E1071931925933744965EEA7E23DCF59A8D1D7A3"
213+
+ "64338294916D37E83C4796283C584751750E39844E2FD97A3727DB5D553F638"
214+
)
215+
216+
KeyValuePair.add_or_update(
217+
KeyValuePairDB(name="k1", value=secret_value, secret=True)
218+
)
219+
220+
lookup = KeyValueLookup()
221+
self.assertRaises(ResourceAccessDeniedError, getattr, lookup, "k1")

st2common/tests/unit/test_util_keyvalue.py

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import mock
1717

1818
import unittest2
19+
from oslo_config import cfg
1920

2021
from st2common.util import keyvalue as kv_utl
2122
from st2common.constants.keyvalue import (
@@ -26,14 +27,30 @@
2627
DATASTORE_PARENT_SCOPE,
2728
DATASTORE_SCOPE_SEPARATOR,
2829
)
30+
from st2common.constants.types import ResourceType
2931
from st2common.exceptions.rbac import AccessDeniedError
32+
from st2common.exceptions.rbac import ResourceAccessDeniedError
3033
from st2common.models.db import auth as auth_db
31-
34+
from st2common.models.db.keyvalue import KeyValuePairDB
35+
from st2common.rbac.backends.noop import NoOpRBACUtils
36+
from st2common.rbac.types import PermissionType
37+
from st2tests import config
3238

3339
USER = "stanley"
40+
RESOURCE_UUID = "%s:%s:%s" % (
41+
ResourceType.KEY_VALUE_PAIR,
42+
FULL_USER_SCOPE,
43+
"stanley:foobar",
44+
)
3445

3546

3647
class TestKeyValueUtil(unittest2.TestCase):
48+
@classmethod
49+
def setUpClass(cls):
50+
super(TestKeyValueUtil, cls).setUpClass()
51+
config.parse_args()
52+
cfg.CONF.set_override(name="backend", override="noop", group="rbac")
53+
3754
def test_validate_scope(self):
3855
scope = FULL_USER_SCOPE
3956
kv_utl._validate_scope(scope)
@@ -126,3 +143,35 @@ def test_get_key(self, deseralize_key_value, KeyValuePair):
126143
def test_get_key_invalid_input(self):
127144
self.assertRaises(TypeError, kv_utl.get_key, key=1)
128145
self.assertRaises(TypeError, kv_utl.get_key, key="test", decrypt="yep")
146+
147+
@mock.patch("st2common.util.keyvalue.KeyValuePair")
148+
@mock.patch("st2common.util.keyvalue.deserialize_key_value")
149+
@mock.patch.object(
150+
NoOpRBACUtils,
151+
"assert_user_has_resource_db_permission",
152+
mock.MagicMock(
153+
side_effect=ResourceAccessDeniedError(
154+
user_db=auth_db.UserDB(name=USER),
155+
resource_api_or_db=KeyValuePairDB(uid=RESOURCE_UUID),
156+
permission_type=PermissionType.KEY_VALUE_PAIR_VIEW,
157+
)
158+
),
159+
)
160+
def test_get_key_unauthorized(self, deseralize_key_value, KeyValuePair):
161+
key, value = ("foobar", "fubar")
162+
decrypt = False
163+
164+
KeyValuePair.get_by_scope_and_name().value = value
165+
deseralize_key_value.return_value = value
166+
167+
self.assertRaises(
168+
ResourceAccessDeniedError,
169+
kv_utl.get_key,
170+
key=key,
171+
user_db=auth_db.UserDB(name=USER),
172+
decrypt=decrypt,
173+
)
174+
175+
KeyValuePair.get_by_scope_and_name.assert_called_with(
176+
FULL_USER_SCOPE, "stanley:%s" % key
177+
)

0 commit comments

Comments
 (0)