Переглянути джерело

HUE-5275 [libsentry] Add support for Solr (V2) Sentry objects to privilege checker

Jenny Kim 9 роки тому
батько
коміт
a899c18

+ 112 - 99
desktop/libs/libsentry/src/libsentry/privilege_checker.py

@@ -17,7 +17,10 @@
 
 import logging
 
-from libsentry.api import get_api
+from collections import defaultdict
+
+from libsentry.api import get_api as get_api_v1
+from libsentry.api2 import get_api as get_api_v2
 
 
 LOG = logging.getLogger(__name__)
@@ -25,10 +28,24 @@ LOG = logging.getLogger(__name__)
 
 PRIVILEGE_HIERARCHY = {
   'SELECT': 0,
+  'QUERY': 0,
+  'READ': 0,  # Not a Sentry privilege, but enables v1 and v2 cross-compatible action type
   'INSERT': 1,
+  'UPDATE': 1,
+  'WRITE': 1,  # Not a Sentry privilege, but enables v1 and v2 cross-compatible action type
   'ALL': 2
 }
 
+SENTRY_OBJECTS = (
+  'SERVER',
+  'DB',
+  'TABLE',
+  'COLUMN',
+  'COLLECTION',
+  'CONFIG',
+  'URI'
+)
+
 SENTRY_PRIVILEGE_KEY = 'SENTRY_PRIVILEGE'
 
 
@@ -37,143 +54,139 @@ class PrivilegeChecker(object):
   Given a user, checks and applies Sentry privilege and authorization rules against Sentry objects
   """
 
-  def __init__(self, user, api=None):
+  def __init__(self, user, api_v1=None, api_v2=None):
     self.user = user
-    self.api = api if api else get_api(self.user)
+    self.api_v1 = api_v1 if api_v1 else get_api_v1(self.user)
+    self.api_v2 = api_v2 if api_v2 else get_api_v2(self.user, component='solr')
 
 
-  def filter_objects(self, authorizableSet, action='SELECT'):
+  def filter_objects(self, authorizableSet, action='READ'):
     """
-    Given a set of authorizable Sentry objects and an action requested, return a filtered set of objects that the user
+    Given a set of authorizable Sentry objects and a requested action, return a filtered set of objects that the user
     has privileges to perform the given action upon.
+    :param authorizableSet: a list of Sentry authorizable objects which can consist of V1 or V2 objects:
+      V1 - {'column': 'total_emp', 'table': 'sample_08', 'db': 'default', 'server': 'server1'}
+      V2 - {'component': 'solr', 'serviceName': 'server1', 'type': 'COLLECTION', 'name': 'twitter_demo'}
+    :param action: requested action-level that we should check privileges against (default: READ)
     """
+    action = action.upper()
     filtered_objects = []
     privileges = []
 
-    user_roles = self.api.list_sentry_roles_by_group('*')  # Get all roles for user
-    for role in user_roles:
-      role_privileges = self.api.list_sentry_privileges_by_role(role['name'])
-      privileges.extend(role_privileges)  # This may result in duplicates but will get reduced in hierarchy tree
+    # Separate V1 (Hive) and V2 (Solr) authorizable objects
+    v1_authorizables = [obj for obj in authorizableSet if 'db' in obj]
+    v2_authorizables = [obj for obj in authorizableSet if 'component' in obj]
 
-    privilege_hierarchy = self._to_privilege_hierarchy(privileges)
-    action = action.upper()
+    if v1_authorizables:
+      user_roles = self.api_v1.list_sentry_roles_by_group('*')  # Get all roles for user
+      for role in user_roles:
+        role_privileges = self.api_v1.list_sentry_privileges_by_role(role['name'])
+        privileges.extend(role_privileges)  # This may result in duplicates but will get reduced in hierarchy tree
+
+      privilege_hierarchy = self._to_privilege_hierarchy_v1(privileges)
 
-    for authorizable in authorizableSet:
-      if self._is_object_action_authorized(hierarchy=privilege_hierarchy, object=authorizable, action=action):
-        filtered_objects.append(authorizable)
+      for authorizable in v1_authorizables:
+        if self._is_object_action_authorized_v1(hierarchy=privilege_hierarchy, object=authorizable, action=action):
+          filtered_objects.append(authorizable)
+
+    if v2_authorizables:
+      privileges = []
+      user_roles = self.api_v2.list_sentry_roles_by_group('*')  # Get all roles for user
+      for role in user_roles:
+        role_privileges = self.api_v2.list_sentry_privileges_by_role(role['name'])
+        privileges.extend(role_privileges)
+
+        privilege_hierarchy = self._to_privilege_hierarchy_v2(privileges)
+
+        for authorizable in v2_authorizables:
+          if self._is_object_action_authorized_v2(hierarchy=privilege_hierarchy, object=authorizable, action=action):
+            filtered_objects.append(authorizable)
 
     return filtered_objects
 
 
-  def _to_privilege_hierarchy(self, privileges):
+  def _to_privilege_hierarchy_v1(self, privileges):
     """
-    TODO: Refactor this to be cleaner and more efficient (use defaultdict and avoid redundant-insertion)
     Converts a list of privileges to a hierarchical tree of privileges by object, where the privilege is stored into a
     key named SENTRY_PRIVILEGE_KEY.
     NOTE: This assumes no objects share the same name as SENTRY_PRIVILEGE_KEY
     """
-    hierarchy = {}
+    tree = lambda: defaultdict(tree)
+    hierarchy = tree()
 
     for privilege in privileges:
-      if privilege['column']:
-        if privilege['server'] not in hierarchy:
-          hierarchy[privilege['server']] = {
-            privilege['database']: {
-              privilege['table']: {
-                privilege['column']: {
-                  SENTRY_PRIVILEGE_KEY: privilege
-                }
-              }
-            }
-          }
-        elif privilege['database'] not in hierarchy[privilege['server']]:
-          hierarchy[privilege['server']][privilege['database']] = {
-            privilege['table']: {
-              privilege['column']: {
-                SENTRY_PRIVILEGE_KEY: privilege
-              }
-            }
-          }
-        elif privilege['table'] not in hierarchy[privilege['server']][privilege['database']]:
-          hierarchy[privilege['server']][privilege['database']][privilege['table']] = {
-            privilege['column']: {
-              SENTRY_PRIVILEGE_KEY: privilege
-            }
-          }
-        else:  # We don't need to check if column is in the hierarchy b/c it's the lowest level object
-          hierarchy[privilege['server']][privilege['database']][privilege['table']][privilege['column']] = {
-            SENTRY_PRIVILEGE_KEY: privilege
-          }
-      elif privilege['table']:
-        if privilege['server'] not in hierarchy:
-          hierarchy[privilege['server']] = {
-            privilege['database']: {
-              privilege['table']: {
-                SENTRY_PRIVILEGE_KEY: privilege
-              }
-            }
-          }
-        elif privilege['database'] not in hierarchy[privilege['server']]:
-          hierarchy[privilege['server']][privilege['database']] = {
-            privilege['table']: {
-              SENTRY_PRIVILEGE_KEY: privilege
-            }
-          }
-        elif privilege['table'] not in hierarchy[privilege['server']][privilege['database']]:
-          hierarchy[privilege['server']][privilege['database']][privilege['table']] = {
-            SENTRY_PRIVILEGE_KEY: privilege
-          }
-        else:
-          hierarchy[privilege['server']][privilege['database']][privilege['table']][SENTRY_PRIVILEGE_KEY] = privilege
-      elif privilege['database']:
-        if privilege['server'] not in hierarchy:
-          hierarchy[privilege['server']] = {
-            privilege['database']: {
-              SENTRY_PRIVILEGE_KEY: privilege
-            }
-          }
-        elif privilege['database'] not in hierarchy[privilege['server']]:
-          hierarchy[privilege['server']][privilege['database']] = {
-            SENTRY_PRIVILEGE_KEY: privilege
-          }
-        else:
-          hierarchy[privilege['server']][privilege['database']][SENTRY_PRIVILEGE_KEY] = privilege
-      else:  # Server scope privilege
-        if privilege['server'] not in hierarchy:
-          hierarchy[privilege['server']] = {
-            SENTRY_PRIVILEGE_KEY: privilege
-          }
-        else:
-          hierarchy[privilege['server']][SENTRY_PRIVILEGE_KEY] = privilege
+      column, table, database, server = privilege['column'], privilege['table'], privilege['database'], privilege['server']
+      if column:
+        hierarchy[server][database][table][column][SENTRY_PRIVILEGE_KEY] = privilege
+      elif table:
+        hierarchy[server][database][table][SENTRY_PRIVILEGE_KEY] = privilege
+      elif database:
+        hierarchy[server][database][SENTRY_PRIVILEGE_KEY] = privilege
+      else:
+        hierarchy[server][SENTRY_PRIVILEGE_KEY] = privilege
 
     return hierarchy
 
 
-  def _is_object_action_authorized(self, hierarchy, object, action='SELECT'):
+  def _to_privilege_hierarchy_v2(self, privileges):
+    """
+    Converts a list of privileges to a hierarchical tree of privileges by object, where the privilege is stored into a
+    key named SENTRY_PRIVILEGE_KEY.
+    NOTE: This assumes no objects share the same name as SENTRY_PRIVILEGE_KEY
+    """
+    tree = lambda: defaultdict(tree)
+    hierarchy = tree()
+
+    for privilege in privileges:
+      component, service, authorizables = privilege['component'], privilege['serviceName'], privilege['authorizables']
+      for obj in authorizables:
+        object_type, object_name = obj.get('type'), obj.get('name')
+        hierarchy[component][service][object_type][object_name][SENTRY_PRIVILEGE_KEY] = privilege
+
+    return hierarchy
+
+
+  def _is_object_action_authorized_v1(self, hierarchy, object, action='READ'):
     requested_action_level = PRIVILEGE_HIERARCHY[action]
-    server, db, table, column = object['server'], object['db'], object['table'], object['column']
 
     # Initialize all privileges for all object levels to non-authorized by default
-    privileges_applied = {
-      'server': -1,
-      'db': -1,
-      'table': -1,
-      'column': -1
-    }
+    privileges_applied = dict((obj, -1) for obj in SENTRY_OBJECTS)
+
+    server, db, table, column = object['server'], object['db'], object['table'], object['column']
 
     if server:  # Get server-level privilege
       if server in hierarchy:
         if SENTRY_PRIVILEGE_KEY in hierarchy[server]:
-          privileges_applied['server'] = PRIVILEGE_HIERARCHY[hierarchy[server][SENTRY_PRIVILEGE_KEY]['action']]
+          privileges_applied['SERVER'] = PRIVILEGE_HIERARCHY[hierarchy[server][SENTRY_PRIVILEGE_KEY]['action']]
         if db and db in hierarchy[server]: # Get db-level privilege
           if SENTRY_PRIVILEGE_KEY in hierarchy[server][db]:
-            privileges_applied['db'] = PRIVILEGE_HIERARCHY[hierarchy[server][db][SENTRY_PRIVILEGE_KEY]['action']]
+            privileges_applied['DB'] = PRIVILEGE_HIERARCHY[hierarchy[server][db][SENTRY_PRIVILEGE_KEY]['action']]
           if table and table in hierarchy[server][db]:  # Get table-level privilege
             if SENTRY_PRIVILEGE_KEY in hierarchy[server][db][table]:
-              privileges_applied['table'] = PRIVILEGE_HIERARCHY[hierarchy[server][db][table][SENTRY_PRIVILEGE_KEY]['action']]
+              privileges_applied['TABLE'] = PRIVILEGE_HIERARCHY[hierarchy[server][db][table][SENTRY_PRIVILEGE_KEY]['action']]
             if column and column in hierarchy[server][db][table]:  # Get column-level privilege
               # Since column is the lowest level, it must have a SENTRY_PRIVILEGE set
-              privileges_applied['column'] = PRIVILEGE_HIERARCHY[hierarchy[server][db][table][column][SENTRY_PRIVILEGE_KEY]['action']]
+              privileges_applied['COLUMN'] = PRIVILEGE_HIERARCHY[hierarchy[server][db][table][column][SENTRY_PRIVILEGE_KEY]['action']]
+
+    # A privilege hierarchy exists and at least one of the granted privileges is greater than or equal to requested action
+    is_authorized = privileges_applied and max(privileges_applied.values()) >= requested_action_level
+    return is_authorized
+
+
+  def _is_object_action_authorized_v2(self, hierarchy, object, action='READ'):
+    requested_action_level = PRIVILEGE_HIERARCHY[action]
+
+    # Initialize all privileges for all object levels to non-authorized by default
+    privileges_applied = dict((obj, -1) for obj in SENTRY_OBJECTS)
+
+    component, service, obj_type, obj_name = object.get('component'), object.get('serviceName'), object.get('type'), object.get('name')
+
+    if component and component in hierarchy:
+      if service and service in hierarchy[component]:
+        if obj_type and obj_type in hierarchy[component][service]:
+          if obj_name and obj_name in hierarchy[component][service][obj_type]:
+            if SENTRY_PRIVILEGE_KEY in hierarchy[component][service][obj_type][obj_name]:
+              privileges_applied[obj_type] = PRIVILEGE_HIERARCHY[hierarchy[component][service][obj_type][obj_name][SENTRY_PRIVILEGE_KEY]['action']]
 
     # A privilege hierarchy exists and at least one of the granted privileges is greater than or equal to requested action
     is_authorized = privileges_applied and max(privileges_applied.values()) >= requested_action_level

+ 38 - 14
desktop/libs/libsentry/src/libsentry/test_privilege_checker.py

@@ -23,7 +23,8 @@ from nose.tools import assert_equal, assert_false, assert_true
 from desktop.lib.django_test_util import make_logged_in_client
 from desktop.lib.test_utils import grant_access
 
-from libsentry.api import get_api, SentryApi
+from libsentry.api import get_api as get_api_v1
+from libsentry.api2 import get_api as get_api_v2
 from libsentry.privilege_checker import PrivilegeChecker
 
 
@@ -34,18 +35,20 @@ class TestDocumentConverter(object):
     self.user = User.objects.get(username="test")
     grant_access("test", "test", "libsentry")
 
-    self.api = get_api(self.user)
-    self.checker = PrivilegeChecker(user=self.user, api=self.api)
+    self.api_v1 = get_api_v1(self.user)
+    self.api_v2 = get_api_v2(self.user, component='solr')
+    self.checker = PrivilegeChecker(user=self.user, api_v1=self.api_v1, api_v2=self.api_v2)
 
 
-  def test_select_privilege(self):
+  def test_read_privilege(self):
     try:
       from mock import Mock
     except ImportError:
       raise SkipTest("Skips until HUE-2947 is resolved")
 
-    action = 'SELECT'
+    action = 'READ'
     authorizableSet = [
+      # V1 authorizables
       {u'column': None, u'table': u'customers', u'db': u'default', u'server': u'server1'},
       {u'column': None, u'table': u'sample_07', u'db': u'default', u'server': u'server1'},
       {u'column': None, u'table': u'sample_08', u'db': u'default', u'server': u'server1'},
@@ -54,25 +57,46 @@ class TestDocumentConverter(object):
       {u'column': 'code', u'table': u'sample_08', u'db': u'default', u'server': u'server1'},
       {u'column': 'total_emp', u'table': u'sample_08', u'db': u'default', u'server': u'server1'},
       {u'column': 'total_emp', u'table': u'sample_07', u'db': u'default', u'server': u'server1'},
+      # V2 authorizables
+      {u'component': u'solr', u'serviceName': u'server1', u'type': u'COLLECTION', u'name': u'logs_demo'},
+      {u'component': u'solr', u'serviceName': u'server1', u'type': u'COLLECTION', u'name': u'test_demo'},
+      {u'component': u'solr', u'serviceName': u'server1', u'type': u'COLLECTION', u'name': u'twitter_demo'},
+      {u'component': u'solr', u'serviceName': u'server1', u'type': u'COLLECTION', u'name': u'yelp_demo'},
+      {u'component': u'solr', u'serviceName': u'server1', u'type': u'CONFIG', u'name': u'twitter_demo'},
+      {u'component': u'solr', u'serviceName': u'server1', u'type': u'CONFIG', u'name': u'managedTemplate'},
+      {u'component': u'solr', u'serviceName': u'server1', u'type': u'CONFIG', u'name': u'yelp_demo'},
     ]
 
-    self.api.list_sentry_roles_by_group = Mock(return_value=[{'name': 'test', 'group': 'test'}])
-    self.api.list_sentry_privileges_by_role = Mock(return_value=[
-        {'column': 'total_emp', 'grantOption': False, 'timestamp': 1478810635378, 'database': 'default', 'action': 'INSERT', 'scope': 'COLUMN', 'table': 'sample_08', 'URI': '', 'server': 'server1'},
-        {'column': '', 'grantOption': False, 'timestamp': 1478810422058, 'database': 'default', 'action': 'SELECT', 'scope': 'TABLE', 'table': 'customers', 'URI': '', 'server': 'server1'},
-        {'column': '', 'grantOption': False, 'timestamp': 1478810513849, 'database': 'default', 'action': 'SELECT', 'scope': 'TABLE', 'table': 'web_logs', 'URI': '', 'server': 'server1'},
-        {'column': '', 'grantOption': False, 'timestamp': 1478810590335, 'database': 'default', 'action': 'SELECT', 'scope': 'TABLE', 'table': 'sample_08', 'URI': '', 'server': 'server1'},
-        {'column': 'salary', 'grantOption': False, 'timestamp': 1478810635396, 'database': 'default', 'action': 'ALL', 'scope': 'COLUMN', 'table': 'sample_08', 'URI': '', 'server': 'server1'}
+    self.api_v1.list_sentry_roles_by_group = Mock(return_value=[{'name': 'test', 'group': 'test'}])
+    self.api_v1.list_sentry_privileges_by_role = Mock(return_value=[
+      {'column': 'total_emp', 'grantOption': False, 'timestamp': 1478810635378, 'database': 'default', 'action': 'INSERT', 'scope': 'COLUMN', 'table': 'sample_08', 'URI': '', 'server': 'server1'},
+      {'column': '', 'grantOption': False, 'timestamp': 1478810422058, 'database': 'default', 'action': 'SELECT', 'scope': 'TABLE', 'table': 'customers', 'URI': '', 'server': 'server1'},
+      {'column': '', 'grantOption': False, 'timestamp': 1478810513849, 'database': 'default', 'action': 'SELECT', 'scope': 'TABLE', 'table': 'web_logs', 'URI': '', 'server': 'server1'},
+      {'column': '', 'grantOption': False, 'timestamp': 1478810590335, 'database': 'default', 'action': 'SELECT', 'scope': 'TABLE', 'table': 'sample_08', 'URI': '', 'server': 'server1'},
+      {'column': 'salary', 'grantOption': False, 'timestamp': 1478810635396, 'database': 'default', 'action': 'ALL', 'scope': 'COLUMN', 'table': 'sample_08', 'URI': '', 'server': 'server1'}
+    ])
+
+    self.api_v2.list_sentry_roles_by_group = Mock(return_value=[{'name': 'test', 'group': 'test'}])
+    self.api_v2.list_sentry_privileges_by_role = Mock(return_value=[
+      {'grantOption': False, 'timestamp': None, 'component': 'solr', 'serviceName': 'server1', 'grantorPrincipal': None, 'action': 'ALL', 'authorizables': [{'type': 'CONFIG', 'name': 'managedTemplate'}]},
+      {'grantOption': False, 'timestamp': None, 'component': 'solr', 'serviceName': 'server1', 'grantorPrincipal': None, 'action': 'QUERY', 'authorizables': [{'type': 'COLLECTION', 'name': 'twitter_demo'}]},
+      {'grantOption': False, 'timestamp': None, 'component': 'solr', 'serviceName': 'server1', 'grantorPrincipal': None, 'action': 'UPDATE', 'authorizables': [{'type': 'COLLECTION', 'name': 'yelp_demo'}]},
+      {'grantOption': False, 'timestamp': None, 'component': 'solr', 'serviceName': 'server1', 'grantorPrincipal': None, 'action': 'ALL', 'authorizables': [{'type': 'CONFIG', 'name': 'yelp_demo'}]}
     ])
 
     filtered_set = self.checker.filter_objects(authorizableSet=authorizableSet, action=action)
     expected_filtered_set = [
+      # V2 authorizables
+      {u'type': u'COLLECTION', u'serviceName': u'server1', u'component': u'solr', u'name': u'twitter_demo'},
+      {u'type': u'COLLECTION', u'serviceName': u'server1', u'component': u'solr', u'name': u'yelp_demo'},
+      {u'type': u'CONFIG', u'serviceName': u'server1', u'component': u'solr', u'name': u'managedTemplate'},
+      {u'type': u'CONFIG', u'serviceName': u'server1', u'component': u'solr', u'name': u'yelp_demo'},
+      # V1 authorizables
       {u'column': None, u'table': u'customers', u'db': u'default', u'server': u'server1'},
       {u'column': None, u'table': u'sample_08', u'db': u'default', u'server': u'server1'},
       {u'column': 'code', u'table': u'sample_08', u'db': u'default', u'server': u'server1'},
       {u'column': 'salary', u'table': u'sample_08', u'db': u'default', u'server': u'server1'},
       {u'column': 'total_emp', u'table': u'sample_08', u'db': u'default', u'server': u'server1'},
       {u'column': None, u'table': u'web_logs', u'db': u'default', u'server': u'server1'},
-
     ]
-    assert_equal(expected_filtered_set, sorted(filtered_set, key=lambda obj: (obj['table'], obj['column'])))
+    assert_equal(expected_filtered_set, sorted(filtered_set, key=lambda obj: (obj.get('table'), obj.get('column'), obj.get('component'), obj.get('type'), obj.get('name'))))