Selaa lähdekoodia

HUE-1362 [metastore] Readonly mode permissions

'metastore:read_only_access' permissions makes users readonly.
Romain Rigaux 12 vuotta sitten
vanhempi
commit
f21df024fe

+ 5 - 1
apps/metastore/src/metastore/settings.py

@@ -21,4 +21,8 @@ REQUIRES_HADOOP = True
 ICON = "/metastore/static/art/table-browser-24-1.png"
 MENU_INDEX = 20
 
-IS_URL_NAMESPACED = True
+IS_URL_NAMESPACED = True
+
+PERMISSION_ACTIONS = (
+  ("read_only_access", "Means you are readonly, no DDL. Need the app access too."),
+)

+ 4 - 0
apps/metastore/src/metastore/templates/databases.mako

@@ -29,9 +29,11 @@ ${ commonheader(_('Databases'), 'metastore', user) | n,unicode }
     <div class="row-fluid">
         <div class="span3">
             <div class="well sidebar-nav">
+              % if has_write_access:
                 <ul class="nav nav-list">
                     <li><a href="${ url('beeswax:create_database') }">${_('Create a new database')}</a></li>
                 </ul>
+              % endif
             </div>
         </div>
         <div class="span9">
@@ -41,7 +43,9 @@ ${ commonheader(_('Databases'), 'metastore', user) | n,unicode }
             </%def>
 
             <%def name="actions()">
+              % if has_write_access:
                 <button id="dropBtn" class="btn toolbarBtn" title="${_('Drop the selected databases')}" disabled="disabled"><i class="icon-trash"></i>  ${_('Drop')}</button>
+              % endif
             </%def>
           </%actionbar:render>
             <table class="table table-condensed table-striped datatables">

+ 1 - 0
apps/metastore/src/metastore/templates/describe_partitions.mako

@@ -25,6 +25,7 @@ ${ commonheader(_('Table Partitions: %(tableName)s') % dict(tableName=table.name
 
 <div class="container-fluid">
   <h1>${_('Partitions')}</h1>
+
   ${ components.breadcrumbs(breadcrumbs) }
 
   <div class="row-fluid">

+ 7 - 0
apps/metastore/src/metastore/templates/describe_table.mako

@@ -53,6 +53,7 @@ ${ commonheader(_("%s : %s") % (view_or_table_noun, table.name), app_name, user)
 
 <div class="container-fluid">
     <h1>${_('Table')} ${table.name}</h1>
+
     ${ components.breadcrumbs(breadcrumbs) }
 
     <div class="row-fluid">
@@ -60,9 +61,13 @@ ${ commonheader(_("%s : %s") % (view_or_table_noun, table.name), app_name, user)
             <div class="well sidebar-nav">
                 <ul class="nav nav-list">
                     <li class="nav-header">${_('Actions')}</li>
+                    % if has_write_access:
                     <li><a href="#" id="import-data-btn">${_('Import Data')}</a></li>
+                    % endif
                     <li><a href="${ url('metastore:read_table', database=database, table=table.name) }">${_('Browse Data')}</a></li>
+                    % if has_write_access:
                     <li><a href="#dropTable" data-toggle="modal">${_('Drop')} ${view_or_table_noun}</a></li>
+                    % endif
                     <li><a href="${ table.hdfs_link }" rel="${ table.path_location }">${_('View File Location')}</a></li>
                     % if table.partition_keys:
                       <li><a href="${ url('metastore:describe_partitions', database=database, table=table.name) }">${_('Show Partitions')} (${ len(partitions) })</a></li>
@@ -181,9 +186,11 @@ ${ commonheader(_("%s : %s") % (view_or_table_noun, table.name), app_name, user)
        }
      });
 
+     % if has_write_access:
      $.getJSON("${ url('metastore:drop_table', database=database) }", function(data) {
        $("#dropTableMessage").text(data.title);
      });
+     % endif
 
      $('a[data-toggle="tab"]').on('shown', function() {
        $(".sampleTable").not('.initialized').addClass('initialized').dataTable({

+ 5 - 0
apps/metastore/src/metastore/templates/tables.mako

@@ -17,6 +17,7 @@
 from desktop.views import commonheader, commonfooter
 from django.utils.translation import ugettext as _
 %>
+
 <%namespace name="actionbar" file="actionbar.mako" />
 <%namespace name="components" file="components.mako" />
 
@@ -37,9 +38,11 @@ ${ commonheader(_('Tables'), 'metastore', user) | n,unicode }
                        </form>
                     </li>
                     </span>
+                    % if has_write_access:
                     <li class="nav-header">${_('Actions')}</li>
                     <li><a href="${ url('beeswax:import_wizard', database=database) }">${_('Create a new table from a file')}</a></li>
                     <li><a href="${ url('beeswax:create_table', database=database) }">${_('Create a new table manually')}</a></li>
+                    % endif
                 </ul>
             </div>
         </div>
@@ -52,7 +55,9 @@ ${ commonheader(_('Tables'), 'metastore', user) | n,unicode }
             <%def name="actions()">
                 <button id="viewBtn" class="btn toolbarBtn" title="${_('Browse the selected table')}" disabled="disabled"><i class="icon-eye-open"></i> ${_('View')}</button>
                 <button id="browseBtn" class="btn toolbarBtn" title="${_('Browse the selected table')}" disabled="disabled"><i class="icon-list"></i> ${_('Browse Data')}</button>
+                % if has_write_access:
                 <button id="dropBtn" class="btn toolbarBtn" title="${_('Delete the selected tables')}" disabled="disabled"><i class="icon-trash"></i>  ${_('Drop')}</button>
+                % endif
             </%def>
           </%actionbar:render>
             <table class="table table-condensed table-striped datatables">

+ 57 - 4
apps/metastore/src/metastore/tests.py

@@ -22,10 +22,14 @@ from nose.tools import assert_true, assert_equal, assert_false
 from nose.plugins.skip import SkipTest
 
 from django.utils.encoding import smart_str
-from django.contrib.auth.models import User
+from django.contrib.auth.models import User, Group
 from django.core.urlresolvers import reverse
 
+import hadoop
 from desktop.lib.django_test_util import make_logged_in_client, assert_equal_mod_whitespace
+from desktop.lib.test_utils import add_permission, grant_access
+from useradmin.models import HuePermission, GroupPermission,\
+  group_has_permission
 
 from beeswax.conf import BROWSE_PARTITIONED_TABLE_LIMIT
 from beeswax.views import collapse_whitespace
@@ -33,7 +37,6 @@ from beeswax.test_base import make_query, wait_for_query_to_finish, verify_histo
 from beeswax.models import QueryHistory
 from beeswax.server import dbms
 from beeswax.test_base import BeeswaxSampleProvider
-import hadoop
 
 
 LOG = logging.getLogger(__name__)
@@ -196,5 +199,55 @@ class TestMetastoreWithHadoop(BeeswaxSampleProvider):
     # Try it with partitions
     resp = self.client.post("/metastore/table/default/test_partitions/load", dict(path="/tmp/foo", partition_0="alpha", partition_1="beta"), follow=True)
     query = QueryHistory.objects.latest('id')
-    assert_equal_mod_whitespace("LOAD DATA INPATH '/tmp/foo' INTO TABLE `default.test_partitions` PARTITION (baz='alpha', boom='beta')",
-        query.query)
+    assert_equal_mod_whitespace("LOAD DATA INPATH '/tmp/foo' INTO TABLE `default.test_partitions` PARTITION (baz='alpha', boom='beta')", query.query)
+
+
+  def test_has_write_access_frontend(self):
+    client = make_logged_in_client(username='write_access_frontend', groupname='write_access_frontend', is_superuser=False)
+    grant_access("write_access_frontend", "write_access_frontend", "metastore")
+    user = User.objects.get(username='write_access_frontend')
+
+    def check(client, assertz):
+      response = client.get("/metastore/databases")
+      assertz("Drop</button>" in response.content, response.content)
+      assertz("Create a new database" in response.content, response.content)
+
+      response = client.get("/metastore/tables/")
+      assertz("Drop</button>" in response.content, response.content)
+      assertz("Create a new table" in response.content, response.content)
+
+    check(client, assert_true)
+
+    # Remove access
+    group, created = Group.objects.get_or_create(name='write_access_frontend')
+    perm, created = HuePermission.objects.get_or_create(app='metastore', action='read_only_access')
+    GroupPermission.objects.get_or_create(group=group, hue_permission=perm)
+
+    check(client, assert_false)
+
+
+  def test_has_write_access_backend(self):
+    client = make_logged_in_client(username='write_access_backend', groupname='write_access_backend', is_superuser=False)
+    grant_access("write_access_backend", "write_access_backend", "metastore")
+    grant_access("write_access_backend", "write_access_backend", "beeswax")
+    user = User.objects.get(username='write_access_backend')
+
+    def check(client, http_code):
+      resp = _make_query(client, 'CREATE TABLE test_perm_1 (a int);')
+      resp = wait_for_query_to_finish(client, resp, max=30.0)
+
+      resp = client.get('/metastore/tables/drop/default', follow=True)
+      #assert_true('want to delete' in resp.content, resp.content)
+      assert_equal(resp.status_code, http_code, resp.content)
+
+      resp = client.post('/metastore/tables/drop/default', {u'table_selection': [u'test_perm_1']}, follow=True)
+      assert_equal(resp.status_code, http_code, resp.content)
+
+    check(client, 200)
+
+    # Remove access
+    group, created = Group.objects.get_or_create(name='write_access_backend')
+    perm, created = HuePermission.objects.get_or_create(app='metastore', action='read_only_access')
+    GroupPermission.objects.get_or_create(group=group, hue_permission=perm)
+
+    check(client, 500)

+ 24 - 0
apps/metastore/src/metastore/views.py

@@ -23,6 +23,7 @@ import logging
 
 from django.http import HttpResponse
 from django.shortcuts import redirect
+from django.utils.functional import wraps
 from django.utils.translation import ugettext as _
 from django.core.urlresolvers import reverse
 
@@ -33,11 +34,24 @@ from beeswax.models import SavedQuery, MetaInstall
 from beeswax.server import dbms
 
 from metastore.forms import LoadDataForm, DbForm
+from metastore.settings import DJANGO_APPS
 
 LOG = logging.getLogger(__name__)
 SAVE_RESULTS_CTAS_TIMEOUT = 300         # seconds
 
 
+def check_has_write_access_permission(view_func):
+  """
+  Decorator ensuring that the user is not a read only user.
+  """
+  def decorate(request, *args, **kwargs):
+    if not has_write_access(request.user):
+      raise PopupException(_('You are not allowed to modify the metastore.'), detail=_('You have metastore:read_only_access permissions'))
+
+    return view_func(request, *args, **kwargs)
+  return wraps(view_func)(decorate)
+
+
 def index(request):
   return redirect(reverse('metastore:show_tables'))
 
@@ -54,9 +68,11 @@ def databases(request):
     'breadcrumbs': [],
     'databases': databases,
     'databases_json': json.dumps(databases),
+    'has_write_access': has_write_access(request.user),
   })
 
 
+@check_has_write_access_permission
 def drop_database(request):
   db = dbms.get(request.user)
 
@@ -110,6 +126,7 @@ def show_tables(request, database=None):
     'db_form': db_form,
     'database': database,
     'tables_json': json.dumps(tables),
+    'has_write_access': has_write_access(request.user),
   })
   resp.set_cookie("hueBeeswaxLastDatabase", database, expires=90)
   return resp
@@ -144,9 +161,11 @@ def describe_table(request, database, table):
     'sample': table_data and table_data.rows(),
     'error_message': error_message,
     'database': database,
+    'has_write_access': has_write_access(request.user),
   })
 
 
+@check_has_write_access_permission
 def drop_table(request, database):
   db = dbms.get(request.user)
 
@@ -191,6 +210,7 @@ def read_partition(request, database, table, partition_id):
     raise PopupException(_('Cannot read table'), detail=e)
 
 
+@check_has_write_access_permission
 def load_table(request, database, table):
   db = dbms.get(request.user)
   table = db.get_table(database, table)
@@ -250,3 +270,7 @@ def describe_partitions(request, database, table):
         },
       ],
       'database': database, 'table': table_obj, 'partitions': partitions, 'request': request})
+
+
+def has_write_access(user):
+  return user.is_superuser or not user.has_hue_permission(action="read_only_access", app=DJANGO_APPS[0])

+ 2 - 1
apps/useradmin/src/useradmin/models.py

@@ -260,7 +260,8 @@ def update_app_permissions(**kwargs):
     default_group = get_default_user_group()
     if default_group:
       for new_dp in added:
-        if not (new_dp.app == 'useradmin' and new_dp.action == 'access'):
+        if not (new_dp.app == 'useradmin' and new_dp.action == 'access') and \
+           not (new_dp.app == 'metastore' and new_dp.action == 'read_only_access'):
           GroupPermission.objects.create(group=default_group, hue_permission=new_dp)
 
     available = HuePermission.objects.count()

+ 9 - 0
docs/user-guide/metastore_manager.md

@@ -193,3 +193,12 @@ Viewing a Table's Location
     window displays.
 2.  Click **View File Location**. The file location of the selected
     table displays in its directory in the File Browser window.
+
+Permissions
+-----------
+
+1.  Ideally install the Sentry project for getting full Hive security.
+2.  Make the Metastore App read only for a certain group of user: In
+    UserAdmin, add the permissions 'read_only_access' to their group.
+
+