Browse Source

HUE-5275 [security] Enable filtered searching of Sentry objects based on privileges

Jenny Kim 9 năm trước cách đây
mục cha
commit
797d6f7

+ 1 - 0
apps/security/src/security/api/hive.py

@@ -22,6 +22,7 @@ import time
 from django.utils.translation import ugettext as _
 
 from desktop.lib.django_util import JsonResponse
+
 from libsentry.api import get_api
 from libsentry.sentry_site import get_sentry_server_admin_groups
 from hadoop.cluster import get_defaultfs

+ 180 - 0
desktop/libs/libsentry/src/libsentry/privilege_checker.py

@@ -0,0 +1,180 @@
+#!/usr/bin/env python
+# Licensed to Cloudera, Inc. under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  Cloudera, Inc. licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import logging
+
+from libsentry.api import get_api
+
+
+LOG = logging.getLogger(__name__)
+
+
+PRIVILEGE_HIERARCHY = {
+  'SELECT': 0,
+  'INSERT': 1,
+  'ALL': 2
+}
+
+SENTRY_PRIVILEGE_KEY = 'SENTRY_PRIVILEGE'
+
+
+class PrivilegeChecker(object):
+  """
+  Given a user, checks and applies Sentry privilege and authorization rules against Sentry objects
+  """
+
+  def __init__(self, user):
+    self.user = user
+    self.api = get_api(self.user)
+    self.user_roles = self.api.list_sentry_roles_by_group('*')  # Get all roles for user
+
+
+  def filter_objects(self, authorizableSet, action='SELECT'):
+    """
+    Given a set of authorizable Sentry objects and an action requested, return a filtered set of objects that the user
+    has privileges to perform the given action upon.
+    """
+    filtered_objects = []
+    privileges = []
+
+    for role in self.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
+
+    privilege_hierarchy = self._to_privilege_hierarchy(privileges)
+    action = action.upper()
+
+    for authorizable in authorizableSet:
+      if self._is_object_action_authorized(hierarchy=privilege_hierarchy, object=authorizable, action=action):
+        filtered_objects.append(authorizable)
+
+    return filtered_objects
+
+
+  def _to_privilege_hierarchy(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 = {}
+
+    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
+
+    return hierarchy
+
+
+  def _is_object_action_authorized(self, hierarchy, object, action='SELECT'):
+    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
+    }
+
+    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']]
+        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']]
+          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']]
+            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']]
+
+    # 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

+ 72 - 0
desktop/libs/libsentry/src/libsentry/test_privilege_checker.py

@@ -0,0 +1,72 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# Licensed to Cloudera, Inc. under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  Cloudera, Inc. licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from django.contrib.auth.models import User
+from nose.plugins.skip import SkipTest
+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 SentryApi
+from libsentry.privilege_checker import PrivilegeChecker
+
+
+class TestDocumentConverter(object):
+
+  @classmethod
+  def setup_class(cls):
+    cls.api_class = SentryApi
+
+  def setUp(self):
+    self.client = make_logged_in_client(username="test", groupname="test", recreate=True, is_superuser=False)
+    self.user = User.objects.get(username="test")
+    grant_access("test", "test", "libsentry")
+
+    self.checker = PrivilegeChecker(user=self.user)
+
+
+  def test_select_privilege(self):
+    try:
+      from mock import Mock
+    except ImportError:
+      raise SkipTest("Skips until HUE-2947 is resolved")
+
+    action = 'SELECT'
+    authorizableSet = [
+      {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'},
+      {u'column': None, u'table': u'web_logs', 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': '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'},
+    ]
+
+    self.api_class.list_sentry_roles_by_group = Mock(return_value=['test'])
+    self.api_class.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'}
+    ])
+
+    filtered_set = self.checker.filter_objects(authorizableSet=authorizableSet, action=action)
+    #assert_equal([], filtered_set)