Skip to content

Commit

Permalink
Merge pull request StackStorm#5354 from ashwini-orchestral/rbac_keyva…
Browse files Browse the repository at this point in the history
…luepair

Implementation of RBAC for KeyValuePair
  • Loading branch information
m4dcoder authored Dec 10, 2021
2 parents 8420fef + d32a7f3 commit d71b157
Show file tree
Hide file tree
Showing 8 changed files with 604 additions and 232 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ in development
Added
~~~~~

* Implemented RBAC functionality for existing ``KEY_VALUE_VIEW, KEY_VALUE_SET, KEY_VALUE_DELETE`` and new permission types ``KEY_VALUE_LIST, KEY_VALUE_ALL``.
RBAC is enabled in the ``st2.conf`` file. Access to a key value pair is checked in the KeyValuePair API controller. #5354

Contributed by @m4dcoder and @ashwini-orchestral

* Added service degerestration on shutdown of a service. #5396

Contributed by @khushboobhatia01
Expand Down
313 changes: 162 additions & 151 deletions st2api/st2api/controllers/v1/keyvalue.py

Large diffs are not rendered by default.

323 changes: 248 additions & 75 deletions st2api/tests/unit/controllers/v1/test_kvps.py

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions st2client/st2client/commands/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
import six
import json
import logging
import traceback

from functools import wraps

Expand Down Expand Up @@ -200,10 +199,11 @@ def get_resource_by_pk(self, pk, **kwargs):
try:
instance = self.manager.get_by_id(pk, **kwargs)
except Exception as e:
traceback.print_exc()
# Hack for "Unauthorized" exceptions, we do want to propagate those
response = getattr(e, "response", None)
status_code = getattr(response, "status_code", None)
if status_code and status_code == http_client.FORBIDDEN:
raise e
if status_code and status_code == http_client.UNAUTHORIZED:
raise e

Expand Down
10 changes: 10 additions & 0 deletions st2common/st2common/rbac/backends/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,16 @@ def user_has_role(user_db, role):
"""
raise NotImplementedError()

@staticmethod
def user_has_system_role(role):
"""
:param user: User object to check for.
:type user: :class:`UserDB`
:rtype: ``bool``
"""
raise NotImplementedError()

@staticmethod
def user_has_rule_trigger_permission(user_db, trigger):
"""
Expand Down
10 changes: 10 additions & 0 deletions st2common/st2common/rbac/backends/noop.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,16 @@ def user_has_role(user_db, role):
"""
return True

@staticmethod
def user_has_system_role(user_db):
"""
:param user: User object to check for.
:type user: :class:`UserDB`
:rtype: ``bool``
"""
return True

@staticmethod
def user_has_rule_trigger_permission(user_db, trigger):
"""
Expand Down
42 changes: 41 additions & 1 deletion st2common/st2common/services/keyvalues.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,15 @@
from st2common.constants.keyvalue import SYSTEM_SCOPE, FULL_SYSTEM_SCOPE
from st2common.constants.keyvalue import USER_SCOPE, FULL_USER_SCOPE
from st2common.constants.keyvalue import ALLOWED_SCOPES
from st2common.constants.keyvalue import DATASTORE_KEY_SEPARATOR
from st2common.constants.keyvalue import DATASTORE_KEY_SEPARATOR, USER_SEPARATOR
from st2common.exceptions.db import StackStormDBObjectNotFoundError
from st2common.exceptions.keyvalue import InvalidScopeException, InvalidUserException
from st2common.models.system.keyvalue import UserKeyReference
from st2common.persistence.keyvalue import KeyValuePair
from st2common.persistence.rbac import UserRoleAssignment
from st2common.persistence.rbac import Role
from st2common.persistence.rbac import PermissionGrant
from st2common.constants.types import ResourceType

__all__ = [
"get_kvp_for_name",
Expand Down Expand Up @@ -256,3 +260,39 @@ def get_key_reference(scope, name, user=None):
raise InvalidScopeException(
'Scope "%s" is not valid. Allowed scopes are %s.' % (scope, ALLOWED_SCOPES)
)


def get_key_uids_for_user(user):
role_names = UserRoleAssignment.query(user=user).only("role").scalar("role")
permission_grant_ids = Role.query(name__in=role_names).scalar("permission_grants")
permission_grant_ids = sum(permission_grant_ids, [])
permission_grants_filters = {}
permission_grants_filters["id__in"] = permission_grant_ids
permission_grants_filters["resource_type"] = ResourceType.KEY_VALUE_PAIR
return PermissionGrant.query(**permission_grants_filters).scalar("resource_uid")


def get_all_system_kvp_names_for_user(user):
"""
Retrieve all the permission grants for a particular user.
The result will return the key list
:rtype: ``list``
"""
key_list = []

for uid in get_key_uids_for_user(user):
pfx = "%s%s%s" % (
ResourceType.KEY_VALUE_PAIR,
DATASTORE_KEY_SEPARATOR,
FULL_SYSTEM_SCOPE,
)
if not uid.startswith(pfx):
continue

key_name = uid.split(DATASTORE_KEY_SEPARATOR)[2:]

if key_name and key_name not in key_list:
key_list.append(USER_SEPARATOR.join(key_name))

return sorted(key_list)
129 changes: 126 additions & 3 deletions st2common/tests/unit/services/test_keyvalue.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,26 @@
# limitations under the License.

from __future__ import absolute_import
import unittest2
from st2tests.api import FunctionalTest

from st2common.constants.keyvalue import SYSTEM_SCOPE, USER_SCOPE
from st2common.constants.keyvalue import SYSTEM_SCOPE, FULL_SYSTEM_SCOPE
from st2common.constants.keyvalue import USER_SCOPE, FULL_USER_SCOPE
from st2common.exceptions.keyvalue import InvalidScopeException, InvalidUserException
from st2common.services.keyvalues import get_key_reference
from st2common.services.keyvalues import get_all_system_kvp_names_for_user
from st2common.persistence.auth import User
from st2common.models.db.auth import UserDB
from st2common.models.db.rbac import UserRoleAssignmentDB
from st2common.models.db.rbac import PermissionGrantDB
from st2common.rbac.types import PermissionType
from st2common.rbac.types import ResourceType
from st2common.persistence.rbac import UserRoleAssignment
from st2common.persistence.rbac import PermissionGrant
from st2common.persistence.rbac import Role
from st2common.models.db.rbac import RoleDB


class KeyValueServicesTest(unittest2.TestCase):
class KeyValueServicesTest(FunctionalTest):
def test_get_key_reference_system_scope(self):
ref = get_key_reference(scope=SYSTEM_SCOPE, name="foo")
self.assertEqual(ref, "foo")
Expand All @@ -41,3 +53,114 @@ def test_get_key_reference_invalid_scope_raises_exception(self):
self.assertRaises(
InvalidScopeException, get_key_reference, scope="sketchy", name="foo"
)

def test_get_all_system_kvp_names_for_user(self):
user1, user2 = "user1", "user2"
kvp_1_uid = "%s:%s:s101" % (ResourceType.KEY_VALUE_PAIR, FULL_SYSTEM_SCOPE)
kvp_2_uid = "%s:%s:s102" % (ResourceType.KEY_VALUE_PAIR, FULL_SYSTEM_SCOPE)
kvp_3_uid = "%s:%s:%s:u101" % (
ResourceType.KEY_VALUE_PAIR,
FULL_USER_SCOPE,
user1,
)
kvp_4_uid = "%s:%s:echo" % (ResourceType.ACTION, "core")
kvp_5_uid = "%s:%s:new_action" % (ResourceType.ACTION, "dummy")
kvp_6_uid = "%s:%s:s103" % (ResourceType.KEY_VALUE_PAIR, FULL_SYSTEM_SCOPE)

# Setup user1, grant, role, and assignment records
user_1_db = UserDB(name=user1)
user_1_db = User.add_or_update(user_1_db)

grant_1_db = PermissionGrantDB(
resource_uid=kvp_1_uid,
resource_type=ResourceType.KEY_VALUE_PAIR,
permission_types=[PermissionType.KEY_VALUE_PAIR_LIST],
)
grant_1_db = PermissionGrant.add_or_update(grant_1_db)

grant_2_db = PermissionGrantDB(
resource_uid=kvp_2_uid,
resource_type=ResourceType.KEY_VALUE_PAIR,
permission_types=[PermissionType.KEY_VALUE_PAIR_VIEW],
)
grant_2_db = PermissionGrant.add_or_update(grant_2_db)

grant_3_db = PermissionGrantDB(
resource_uid=kvp_3_uid,
resource_type=ResourceType.KEY_VALUE_PAIR,
permission_types=[PermissionType.KEY_VALUE_PAIR_ALL],
)
grant_3_db = PermissionGrant.add_or_update(grant_3_db)

grant_4_db = PermissionGrantDB(
resource_uid=kvp_4_uid,
resource_type=ResourceType.ACTION,
permission_types=[PermissionType.ACTION_VIEW],
)
grant_4_db = PermissionGrant.add_or_update(grant_4_db)

grant_5_db = PermissionGrantDB(
resource_uid=kvp_5_uid,
resource_type=ResourceType.ACTION,
permission_types=[PermissionType.ACTION_LIST],
)
grant_5_db = PermissionGrant.add_or_update(grant_5_db)

role_1_db = RoleDB(
name="user1_custom_role_grant",
permission_grants=[
str(grant_1_db.id),
str(grant_2_db.id),
str(grant_3_db.id),
str(grant_4_db.id),
],
)
role_1_db = Role.add_or_update(role_1_db)

role_1_assignment_db = UserRoleAssignmentDB(
user=user_1_db.name,
role=role_1_db.name,
source="assignments/%s.yaml" % user_1_db.name,
)
UserRoleAssignment.add_or_update(role_1_assignment_db)

# Setup user2, grant, role, and assignment records
user_2_db = UserDB(name=user2)
user_2_db = User.add_or_update(user_2_db)

grant_6_db = PermissionGrantDB(
resource_uid=kvp_6_uid,
resource_type=ResourceType.KEY_VALUE_PAIR,
permission_types=[PermissionType.KEY_VALUE_PAIR_ALL],
)
grant_6_db = PermissionGrant.add_or_update(grant_6_db)

role_2_db = RoleDB(
name="user2_custom_role_grant",
permission_grants=[
str(grant_5_db.id),
str(grant_6_db.id),
],
)
role_2_db = Role.add_or_update(role_2_db)

role_2_assignment_db = UserRoleAssignmentDB(
user=user_2_db.name,
role=role_2_db.name,
source="assignments/%s.yaml" % user_2_db.name,
)
UserRoleAssignment.add_or_update(role_2_assignment_db)

# Assert result of get_all_system_kvp_names_for_user for user1
# The uids for non key value pair resource type should not be included in the result.
# The user scoped key should not be included in the result.
actual_result = get_all_system_kvp_names_for_user(user=user_1_db.name)
expected_result = ["s101", "s102"]
self.assertListEqual(actual_result, expected_result)

# Assert result of get_all_system_kvp_names_for_user for user2
# The uids for non key value pair resource type should not be included in the result.
# The user scoped key should not be included in the result.
actual_result = get_all_system_kvp_names_for_user(user=user_2_db.name)
expected_result = ["s103"]
self.assertListEqual(actual_result, expected_result)

0 comments on commit d71b157

Please sign in to comment.