Skip to content

Commit d71b157

Browse files
authored
Merge pull request #5354 from ashwini-orchestral/rbac_keyvaluepair
Implementation of RBAC for KeyValuePair
2 parents 8420fef + d32a7f3 commit d71b157

File tree

8 files changed

+604
-232
lines changed

8 files changed

+604
-232
lines changed

CHANGELOG.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ in development
77
Added
88
~~~~~
99

10+
* Implemented RBAC functionality for existing ``KEY_VALUE_VIEW, KEY_VALUE_SET, KEY_VALUE_DELETE`` and new permission types ``KEY_VALUE_LIST, KEY_VALUE_ALL``.
11+
RBAC is enabled in the ``st2.conf`` file. Access to a key value pair is checked in the KeyValuePair API controller. #5354
12+
13+
Contributed by @m4dcoder and @ashwini-orchestral
14+
1015
* Added service degerestration on shutdown of a service. #5396
1116

1217
Contributed by @khushboobhatia01

st2api/st2api/controllers/v1/keyvalue.py

Lines changed: 162 additions & 151 deletions
Large diffs are not rendered by default.

st2api/tests/unit/controllers/v1/test_kvps.py

Lines changed: 248 additions & 75 deletions
Large diffs are not rendered by default.

st2client/st2client/commands/resource.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
import six
2525
import json
2626
import logging
27-
import traceback
2827

2928
from functools import wraps
3029

@@ -200,10 +199,11 @@ def get_resource_by_pk(self, pk, **kwargs):
200199
try:
201200
instance = self.manager.get_by_id(pk, **kwargs)
202201
except Exception as e:
203-
traceback.print_exc()
204202
# Hack for "Unauthorized" exceptions, we do want to propagate those
205203
response = getattr(e, "response", None)
206204
status_code = getattr(response, "status_code", None)
205+
if status_code and status_code == http_client.FORBIDDEN:
206+
raise e
207207
if status_code and status_code == http_client.UNAUTHORIZED:
208208
raise e
209209

st2common/st2common/rbac/backends/base.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,16 @@ def user_has_role(user_db, role):
229229
"""
230230
raise NotImplementedError()
231231

232+
@staticmethod
233+
def user_has_system_role(role):
234+
"""
235+
:param user: User object to check for.
236+
:type user: :class:`UserDB`
237+
238+
:rtype: ``bool``
239+
"""
240+
raise NotImplementedError()
241+
232242
@staticmethod
233243
def user_has_rule_trigger_permission(user_db, trigger):
234244
"""

st2common/st2common/rbac/backends/noop.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,16 @@ def user_has_role(user_db, role):
195195
"""
196196
return True
197197

198+
@staticmethod
199+
def user_has_system_role(user_db):
200+
"""
201+
:param user: User object to check for.
202+
:type user: :class:`UserDB`
203+
204+
:rtype: ``bool``
205+
"""
206+
return True
207+
198208
@staticmethod
199209
def user_has_rule_trigger_permission(user_db, trigger):
200210
"""

st2common/st2common/services/keyvalues.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,15 @@
2121
from st2common.constants.keyvalue import SYSTEM_SCOPE, FULL_SYSTEM_SCOPE
2222
from st2common.constants.keyvalue import USER_SCOPE, FULL_USER_SCOPE
2323
from st2common.constants.keyvalue import ALLOWED_SCOPES
24-
from st2common.constants.keyvalue import DATASTORE_KEY_SEPARATOR
24+
from st2common.constants.keyvalue import DATASTORE_KEY_SEPARATOR, USER_SEPARATOR
2525
from st2common.exceptions.db import StackStormDBObjectNotFoundError
2626
from st2common.exceptions.keyvalue import InvalidScopeException, InvalidUserException
2727
from st2common.models.system.keyvalue import UserKeyReference
2828
from st2common.persistence.keyvalue import KeyValuePair
29+
from st2common.persistence.rbac import UserRoleAssignment
30+
from st2common.persistence.rbac import Role
31+
from st2common.persistence.rbac import PermissionGrant
32+
from st2common.constants.types import ResourceType
2933

3034
__all__ = [
3135
"get_kvp_for_name",
@@ -256,3 +260,39 @@ def get_key_reference(scope, name, user=None):
256260
raise InvalidScopeException(
257261
'Scope "%s" is not valid. Allowed scopes are %s.' % (scope, ALLOWED_SCOPES)
258262
)
263+
264+
265+
def get_key_uids_for_user(user):
266+
role_names = UserRoleAssignment.query(user=user).only("role").scalar("role")
267+
permission_grant_ids = Role.query(name__in=role_names).scalar("permission_grants")
268+
permission_grant_ids = sum(permission_grant_ids, [])
269+
permission_grants_filters = {}
270+
permission_grants_filters["id__in"] = permission_grant_ids
271+
permission_grants_filters["resource_type"] = ResourceType.KEY_VALUE_PAIR
272+
return PermissionGrant.query(**permission_grants_filters).scalar("resource_uid")
273+
274+
275+
def get_all_system_kvp_names_for_user(user):
276+
"""
277+
Retrieve all the permission grants for a particular user.
278+
The result will return the key list
279+
280+
:rtype: ``list``
281+
"""
282+
key_list = []
283+
284+
for uid in get_key_uids_for_user(user):
285+
pfx = "%s%s%s" % (
286+
ResourceType.KEY_VALUE_PAIR,
287+
DATASTORE_KEY_SEPARATOR,
288+
FULL_SYSTEM_SCOPE,
289+
)
290+
if not uid.startswith(pfx):
291+
continue
292+
293+
key_name = uid.split(DATASTORE_KEY_SEPARATOR)[2:]
294+
295+
if key_name and key_name not in key_list:
296+
key_list.append(USER_SEPARATOR.join(key_name))
297+
298+
return sorted(key_list)

st2common/tests/unit/services/test_keyvalue.py

Lines changed: 126 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,26 @@
1414
# limitations under the License.
1515

1616
from __future__ import absolute_import
17-
import unittest2
17+
from st2tests.api import FunctionalTest
1818

19-
from st2common.constants.keyvalue import SYSTEM_SCOPE, USER_SCOPE
19+
from st2common.constants.keyvalue import SYSTEM_SCOPE, FULL_SYSTEM_SCOPE
20+
from st2common.constants.keyvalue import USER_SCOPE, FULL_USER_SCOPE
2021
from st2common.exceptions.keyvalue import InvalidScopeException, InvalidUserException
2122
from st2common.services.keyvalues import get_key_reference
23+
from st2common.services.keyvalues import get_all_system_kvp_names_for_user
24+
from st2common.persistence.auth import User
25+
from st2common.models.db.auth import UserDB
26+
from st2common.models.db.rbac import UserRoleAssignmentDB
27+
from st2common.models.db.rbac import PermissionGrantDB
28+
from st2common.rbac.types import PermissionType
29+
from st2common.rbac.types import ResourceType
30+
from st2common.persistence.rbac import UserRoleAssignment
31+
from st2common.persistence.rbac import PermissionGrant
32+
from st2common.persistence.rbac import Role
33+
from st2common.models.db.rbac import RoleDB
2234

2335

24-
class KeyValueServicesTest(unittest2.TestCase):
36+
class KeyValueServicesTest(FunctionalTest):
2537
def test_get_key_reference_system_scope(self):
2638
ref = get_key_reference(scope=SYSTEM_SCOPE, name="foo")
2739
self.assertEqual(ref, "foo")
@@ -41,3 +53,114 @@ def test_get_key_reference_invalid_scope_raises_exception(self):
4153
self.assertRaises(
4254
InvalidScopeException, get_key_reference, scope="sketchy", name="foo"
4355
)
56+
57+
def test_get_all_system_kvp_names_for_user(self):
58+
user1, user2 = "user1", "user2"
59+
kvp_1_uid = "%s:%s:s101" % (ResourceType.KEY_VALUE_PAIR, FULL_SYSTEM_SCOPE)
60+
kvp_2_uid = "%s:%s:s102" % (ResourceType.KEY_VALUE_PAIR, FULL_SYSTEM_SCOPE)
61+
kvp_3_uid = "%s:%s:%s:u101" % (
62+
ResourceType.KEY_VALUE_PAIR,
63+
FULL_USER_SCOPE,
64+
user1,
65+
)
66+
kvp_4_uid = "%s:%s:echo" % (ResourceType.ACTION, "core")
67+
kvp_5_uid = "%s:%s:new_action" % (ResourceType.ACTION, "dummy")
68+
kvp_6_uid = "%s:%s:s103" % (ResourceType.KEY_VALUE_PAIR, FULL_SYSTEM_SCOPE)
69+
70+
# Setup user1, grant, role, and assignment records
71+
user_1_db = UserDB(name=user1)
72+
user_1_db = User.add_or_update(user_1_db)
73+
74+
grant_1_db = PermissionGrantDB(
75+
resource_uid=kvp_1_uid,
76+
resource_type=ResourceType.KEY_VALUE_PAIR,
77+
permission_types=[PermissionType.KEY_VALUE_PAIR_LIST],
78+
)
79+
grant_1_db = PermissionGrant.add_or_update(grant_1_db)
80+
81+
grant_2_db = PermissionGrantDB(
82+
resource_uid=kvp_2_uid,
83+
resource_type=ResourceType.KEY_VALUE_PAIR,
84+
permission_types=[PermissionType.KEY_VALUE_PAIR_VIEW],
85+
)
86+
grant_2_db = PermissionGrant.add_or_update(grant_2_db)
87+
88+
grant_3_db = PermissionGrantDB(
89+
resource_uid=kvp_3_uid,
90+
resource_type=ResourceType.KEY_VALUE_PAIR,
91+
permission_types=[PermissionType.KEY_VALUE_PAIR_ALL],
92+
)
93+
grant_3_db = PermissionGrant.add_or_update(grant_3_db)
94+
95+
grant_4_db = PermissionGrantDB(
96+
resource_uid=kvp_4_uid,
97+
resource_type=ResourceType.ACTION,
98+
permission_types=[PermissionType.ACTION_VIEW],
99+
)
100+
grant_4_db = PermissionGrant.add_or_update(grant_4_db)
101+
102+
grant_5_db = PermissionGrantDB(
103+
resource_uid=kvp_5_uid,
104+
resource_type=ResourceType.ACTION,
105+
permission_types=[PermissionType.ACTION_LIST],
106+
)
107+
grant_5_db = PermissionGrant.add_or_update(grant_5_db)
108+
109+
role_1_db = RoleDB(
110+
name="user1_custom_role_grant",
111+
permission_grants=[
112+
str(grant_1_db.id),
113+
str(grant_2_db.id),
114+
str(grant_3_db.id),
115+
str(grant_4_db.id),
116+
],
117+
)
118+
role_1_db = Role.add_or_update(role_1_db)
119+
120+
role_1_assignment_db = UserRoleAssignmentDB(
121+
user=user_1_db.name,
122+
role=role_1_db.name,
123+
source="assignments/%s.yaml" % user_1_db.name,
124+
)
125+
UserRoleAssignment.add_or_update(role_1_assignment_db)
126+
127+
# Setup user2, grant, role, and assignment records
128+
user_2_db = UserDB(name=user2)
129+
user_2_db = User.add_or_update(user_2_db)
130+
131+
grant_6_db = PermissionGrantDB(
132+
resource_uid=kvp_6_uid,
133+
resource_type=ResourceType.KEY_VALUE_PAIR,
134+
permission_types=[PermissionType.KEY_VALUE_PAIR_ALL],
135+
)
136+
grant_6_db = PermissionGrant.add_or_update(grant_6_db)
137+
138+
role_2_db = RoleDB(
139+
name="user2_custom_role_grant",
140+
permission_grants=[
141+
str(grant_5_db.id),
142+
str(grant_6_db.id),
143+
],
144+
)
145+
role_2_db = Role.add_or_update(role_2_db)
146+
147+
role_2_assignment_db = UserRoleAssignmentDB(
148+
user=user_2_db.name,
149+
role=role_2_db.name,
150+
source="assignments/%s.yaml" % user_2_db.name,
151+
)
152+
UserRoleAssignment.add_or_update(role_2_assignment_db)
153+
154+
# Assert result of get_all_system_kvp_names_for_user for user1
155+
# The uids for non key value pair resource type should not be included in the result.
156+
# The user scoped key should not be included in the result.
157+
actual_result = get_all_system_kvp_names_for_user(user=user_1_db.name)
158+
expected_result = ["s101", "s102"]
159+
self.assertListEqual(actual_result, expected_result)
160+
161+
# Assert result of get_all_system_kvp_names_for_user for user2
162+
# The uids for non key value pair resource type should not be included in the result.
163+
# The user scoped key should not be included in the result.
164+
actual_result = get_all_system_kvp_names_for_user(user=user_2_db.name)
165+
expected_result = ["s103"]
166+
self.assertListEqual(actual_result, expected_result)

0 commit comments

Comments
 (0)