Forráskód Böngészése

HUE-1987 [search] Dashboard level access for non admin

Superuser can access everything.
Non superuser can create their own dashboard and share them.
Ony a superuser or an owner can modify a dashboard.
Only dashboard shared by an admin or own user dashboard will appear in top menu.
Shortcut for creating a dashboard in the top menu.
isEditing is set to true automatically on new dashboards.

Finer grain permissions will come in Hue 4 and https://issues.cloudera.org/browse/HUE-2584
Romain Rigaux 10 éve
szülő
commit
b74007f

+ 21 - 9
apps/search/src/search/decorators.py

@@ -24,30 +24,42 @@ from django.utils.translation import ugettext as _
 from desktop.lib.exceptions_renderable import PopupException
 
 from search.models import Collection
+from search.search_controller import SearchController
+
 
 LOG = logging.getLogger(__name__)
 
 
-def allow_admin_only(view_func):
+def allow_viewer_only(view_func):
   def decorate(request, *args, **kwargs):
 
-    if not request.user.is_superuser:
-      message = _("Permission denied. You are not an Administrator.")
-      raise PopupException(message)
+    collection_json = json.loads(request.POST.get('collection', '{}'))
+
+    if collection_json['id']:
+      try:
+        SearchController(request.user).get_search_collections().get(id=collection_json['id'])
+      except Collection.DoesNotExist:
+        message = _("Dashboard does not exist or you don't have the permission to access it.")
+        raise PopupException(message)
 
     return view_func(request, *args, **kwargs)
   return wraps(view_func)(decorate)
 
 
-def allow_writer_only(view_func):
+def allow_owner_only(view_func):
   def decorate(request, *args, **kwargs):
 
     collection_json = json.loads(request.POST.get('collection', '{}'))
-    collection = Collection.objects.get(id=collection_json['id']) # TODO perms with doc model HUE-1987
 
-    if not request.user.is_superuser:
-      message = _("Permission denied. You are not an Administrator.")
-      raise PopupException(message)
+    if collection_json['id']:
+      try:
+        collection = Collection.objects.get(id=collection_json['id'])
+
+        if collection.owner != request.user and not request.user.is_superuser:
+          message = _("Permission denied. You are not an Administrator.")
+          raise PopupException(message)
+      except Collection.DoesNotExist:
+        pass
 
     return view_func(request, *args, **kwargs)
   return wraps(view_func)(decorate)

+ 91 - 0
apps/search/src/search/migrations/0003_auto__add_field_collection_owner.py

@@ -0,0 +1,91 @@
+# -*- coding: utf-8 -*-
+from south.utils import datetime_utils as datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        # Adding field 'Collection.owner'
+        db.add_column(u'search_collection', 'owner',
+                      self.gf('django.db.models.fields.related.ForeignKey')(default=None, to=orm['auth.User'], null=True),
+                      keep_default=False)
+
+
+    def backwards(self, orm):
+        # Deleting field 'Collection.owner'
+        db.delete_column(u'search_collection', 'owner_id')
+
+
+    models = {
+        u'auth.group': {
+            'Meta': {'object_name': 'Group'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+        },
+        u'auth.permission': {
+            'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        u'auth.user': {
+            'Meta': {'object_name': 'User'},
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+        },
+        u'contenttypes.contenttype': {
+            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        },
+        u'search.collection': {
+            'Meta': {'object_name': 'Collection'},
+            'cores': ('django.db.models.fields.TextField', [], {'default': "'{}'"}),
+            'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'facets': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['search.Facet']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_core_only': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
+            'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['auth.User']", 'null': 'True'}),
+            'properties': ('django.db.models.fields.TextField', [], {'default': "'{}'"}),
+            'result': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['search.Result']"}),
+            'sorting': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['search.Sorting']"})
+        },
+        u'search.facet': {
+            'Meta': {'object_name': 'Facet'},
+            'data': ('django.db.models.fields.TextField', [], {}),
+            'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+        },
+        u'search.result': {
+            'Meta': {'object_name': 'Result'},
+            'data': ('django.db.models.fields.TextField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+        },
+        u'search.sorting': {
+            'Meta': {'object_name': 'Sorting'},
+            'data': ('django.db.models.fields.TextField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+        }
+    }
+
+    complete_apps = ['search']

+ 9 - 3
apps/search/src/search/models.py

@@ -21,10 +21,12 @@ import logging
 import numbers
 import re
 
+from django.contrib.auth.models import User
+from django.core.urlresolvers import reverse
 from django.db import models
 from django.utils.html import escape
 from django.utils.translation import ugettext as _, ugettext_lazy as _t
-from django.core.urlresolvers import reverse
+
 
 from desktop.lib.i18n import smart_unicode
 
@@ -205,7 +207,7 @@ class Sorting(models.Model):
 
 class CollectionManager(models.Manager):
 
-  def create2(self, name, label, is_core_only=False):
+  def create2(self, name, label, is_core_only=False, owner=None):
     facets = Facet.objects.create()
     result = Result.objects.create()
     sorting = Sorting.objects.create()
@@ -213,6 +215,8 @@ class CollectionManager(models.Manager):
     collection = Collection.objects.create(
         name=name,
         label=label,
+        owner=owner,
+        enabled=False,
         cores=json.dumps({'version': 2}),
         is_core_only=is_core_only,
         facets=facets,
@@ -225,7 +229,7 @@ class CollectionManager(models.Manager):
 
 class Collection(models.Model):
   """All the data is now saved into the properties field"""
-  enabled = models.BooleanField(default=True)
+  enabled = models.BooleanField(default=False) # Aka shared
   name = models.CharField(max_length=40, verbose_name=_t('Solr index name pointing to'))
   label = models.CharField(max_length=100, verbose_name=_t('Friendlier name in UI'))
   is_core_only = models.BooleanField(default=False)
@@ -239,6 +243,8 @@ class Collection(models.Model):
   result = models.ForeignKey(Result)
   sorting = models.ForeignKey(Sorting)
 
+  owner = models.ForeignKey(User, db_index=True, verbose_name=_t('Owner'), help_text=_t('User who created the job.'), default=None, null=True)
+
   _ATTRIBUTES = ['collection', 'layout', 'autocomplete']
   ICON = 'search/art/icon_search_48.png'
 

+ 17 - 4
apps/search/src/search/search_controller.py

@@ -18,6 +18,8 @@
 
 import logging
 
+from django.contrib.auth.models import User
+from django.db.models import Q
 from django.utils.translation import ugettext as _
 
 from libsolr.api import SolrApi
@@ -37,13 +39,24 @@ class SearchController(object):
     self.user = user
 
   def get_search_collections(self):
-    # TODO perms
-    return Collection.objects.filter(enabled=True)
+    if self.user.is_superuser:
+      return Collection.objects.all().order_by('-id')
+    else:
+      return Collection.objects.filter(Q(owner=self.user) | Q(enabled=True)).order_by('-id')
+
+  def get_shared_search_collections(self):
+    return Collection.objects.filter(Q(owner=self.user) | Q(enabled=True, owner__in=User.objects.filter(is_superuser=True))).order_by('-id')
+
+  def get_owner_search_collections(self):
+    if self.user.is_superuser:
+      return Collection.objects.all()
+    else:
+      return Collection.objects.filter(Q(owner=self.user))
 
   def delete_collections(self, collection_ids):
     result = {'status': -1, 'message': ''}
     try:
-      Collection.objects.filter(id__in=collection_ids).delete()
+      self.get_owner_search_collections().filter(id__in=collection_ids).delete()
       result['status'] = 0
     except Exception, e:
       LOG.warn('Error deleting collection: %s' % e)
@@ -54,7 +67,7 @@ class SearchController(object):
   def copy_collections(self, collection_ids):
     result = {'status': -1, 'message': ''}
     try:
-      for collection in Collection.objects.filter(id__in=collection_ids):
+      for collection in self.get_shared_search_collections().filter(id__in=collection_ids):
         copy = collection
         copy.label += _(' (Copy)')
         copy.id = copy.pk = None

+ 8 - 1
apps/search/src/search/static/search/js/collections.ko.js

@@ -33,6 +33,8 @@ var Collection = function (coll) {
   self.enabled = ko.observable(coll.enabled);
   self.isCoreOnly = ko.observable(coll.isCoreOnly);
   self.absoluteUrl = ko.observable(coll.absoluteUrl);
+  self.owner = ko.observable(coll.owner);
+  self.isOwner = ko.observable(coll.isOwner);
   self.selected = ko.observable(false);
   self.hovered = ko.observable(false);
 
@@ -78,9 +80,14 @@ var SearchCollectionsModel = function (props) {
       return coll.selected();
     });
   }, self);
+  self.selectedOwnerCollections = ko.computed(function () {
+    return ko.utils.arrayFilter(self.selectedCollections(), function (coll) {
+      return coll.isOwner();
+    });
+  }, self);
 
   self.atLeastOneSelected = ko.computed(function() {
-    return self.selectedCollections().length >= 1;
+    return self.selectedCollections().length >= 1 && self.selectedCollections().length == self.selectedOwnerCollections().length;
   });
 
   self.selectedImportableCollections = ko.computed(function () {

+ 3 - 2
apps/search/src/search/static/search/js/search.ko.js

@@ -392,7 +392,7 @@ var FieldAnalysis = function (vm, field_name) {
 var Collection = function (vm, collection) {
   var self = this;
 
-  self.id = collection.id;
+  self.id = ko.mapping.fromJS(collection.id);
   self.name = ko.mapping.fromJS(collection.name);
   self.label = ko.mapping.fromJS(collection.label);
   self.enabled = ko.mapping.fromJS(collection.enabled);
@@ -1058,6 +1058,7 @@ var SearchViewModel = function (collection_json, query_json, initial_json) {
 
 
   self.init = function (callback) {
+    self.isEditing(self.columns().length == 0);
     self.initial.init();
     self.collection.syncFields();
     self.search(callback);
@@ -1294,7 +1295,7 @@ var SearchViewModel = function (collection_json, query_json, initial_json) {
       layout: ko.mapping.toJSON(self.columns)
     }, function (data) {
       if (data.status == 0) {
-        self.collection.id = data.id;
+        self.collection.id(data.id);
         $(document).trigger("info", data.message);
         if (window.location.search.indexOf("collection") == -1) {
           window.location.hash = '#collection=' + data.id;

+ 2 - 0
apps/search/src/search/templates/admin_collections.mako

@@ -80,6 +80,7 @@ ${ commonheader(_('Search'), "search", user, "29px") | n,unicode }
               </th>
               <th>${ _('Name') }</th>
               <th>${ _('Solr Index') }</th>
+              <th width="15%">${ _('Owner') }</th>
               <th width="1%" class="center">${ _('Shared') }</th>
             </tr>
           </thead>
@@ -90,6 +91,7 @@ ${ commonheader(_('Search'), "search", user, "29px") | n,unicode }
               </td>
               <td><a data-bind="text: label, click: $root.editCollection" title="${ _('Click to edit') }" class="pointer"></a></td>
               <td><a data-bind="text: name, click: $root.editIndex" title="${ _('Click to edit the index') }" class="pointer"></a></td>
+              <td><span data-bind="text: owner"></span></td>
               <td class="center"><span data-bind="css: { 'fa fa-check': enabled }"></span></td>
             </tr>
           </tbody>

+ 32 - 14
apps/search/src/search/templates/search.mako

@@ -33,20 +33,35 @@ ${ commonheader(_('Search'), "search", user, "80px") | n,unicode }
 
 <div class="search-bar">
   <div class="pull-right" style="padding-right:50px">
-    % if user.is_superuser:
-      <button type="button" title="${ _('Edit') }" rel="tooltip" data-placement="bottom" data-bind="click: toggleEditing, css: {'btn': true, 'btn-inverse': isEditing}"><i class="fa fa-pencil"></i></button>
-      <button type="button" title="${ _('Save') }" rel="tooltip" data-placement="bottom" data-loading-text="${ _("Saving...") }" data-bind="click: save, css: {'btn': true}"><i class="fa fa-save"></i></button>
-      <button type="button" title="${ _('Settings') }" rel="tooltip" data-placement="bottom" data-toggle="modal" data-target="#settingsDemiModal"
-          data-bind="css: {'btn': true}">
-        <i class="fa fa-cog"></i>
-      </button>
-    % endif
-      <button type="button" title="${ _('Share') }" rel="tooltip" data-placement="bottom" data-bind="click: showShareModal, css: {'btn': true}"><i class="fa fa-link"></i></button>
-    % if user.is_superuser:
-      &nbsp;&nbsp;&nbsp;
-      <a class="btn" href="${ url('search:new_search') }" title="${ _('New') }" rel="tooltip" data-placement="bottom" data-bind="css: {'btn': true}"><i class="fa fa-file-o"></i></a>
-      <a class="btn" href="${ url('search:admin_collections') }" title="${ _('Dashboards') }" rel="tooltip" data-placement="bottom" data-bind="css: {'btn': true}"><i class="fa fa-tags"></i></a>
+    <button type="button" title="${ _('Edit') }" rel="tooltip" data-placement="bottom" data-bind="click: toggleEditing, css: {'btn': true, 'btn-inverse': isEditing}">
+      <i class="fa fa-pencil"></i>
+    </button>
+    % if is_owner:
+    <button type="button" title="${ _('Save') }" rel="tooltip" data-placement="bottom" data-loading-text="${ _("Saving...") }" data-bind="click: save, css: {'btn': true}, visible: columns().length != 0">
+      <i class="fa fa-save"></i>
+    </button>
     % endif
+    <button type="button" title="${ _('Settings') }" rel="tooltip" data-placement="bottom" data-toggle="modal" data-target="#settingsDemiModal"
+        data-bind="css: {'btn': true}, visible: columns().length != 0">
+      <i class="fa fa-cog"></i>
+    </button>
+
+    <span style="padding-left:85px" data-bind="visible: columns().length == 0"></span>
+
+    &nbsp;&nbsp;&nbsp;
+
+    <button type="button" title="${ _('Share') }" rel="tooltip" data-placement="bottom" data-bind="click: showShareModal, css: {'btn': true}, visible: columns().length != 0, enable: $root.collection.id() != null">
+      <i class="fa fa-link"></i>
+    </button>
+
+    &nbsp;&nbsp;&nbsp;
+
+    <a class="btn" href="${ url('search:new_search') }" title="${ _('New') }" rel="tooltip" data-placement="bottom" data-bind="css: {'btn': true}">
+      <i class="fa fa-file-o"></i>
+    </a>
+    <a class="btn" href="${ url('search:admin_collections') }" title="${ _('Dashboards') }" rel="tooltip" data-placement="bottom" data-bind="css: {'btn': true}">
+      <i class="fa fa-tags"></i>
+    </a>
   </div>
 
   <form data-bind="visible: $root.isEditing() && columns().length == 0">
@@ -54,7 +69,10 @@ ${ commonheader(_('Search'), "search", user, "80px") | n,unicode }
     <!-- ko if: columns().length == 0 -->
     <select data-bind="options: $root.initial.collections, value: $root.collection.name, disable: isSyncingCollections">
     </select>
-    <label class="checkbox" style="display:inline-block; margin-left: 10px"><input type="checkbox" data-bind="checked: showCores" />${ _('Show cores') } <i class="fa fa-spinner fa-spin" data-bind="visible: isSyncingCollections"></i></label>
+    <label class="checkbox" style="display:inline-block; margin-left: 10px">
+      <input type="checkbox" data-bind="checked: showCores" />${ _('Show cores') }
+      <i class="fa fa-spinner fa-spin" data-bind="visible: isSyncingCollections"></i>
+    </label>
     <!-- /ko -->
   </form>
 

+ 1 - 1
apps/search/src/search/tests.py

@@ -61,7 +61,7 @@ class TestSearchBase(object):
     grant_access('test_search', 'test_search', 'search')
     self.user = User.objects.get(username='test_search')
     try:
-      self.collection = Collection.objects.create2(name='collection_1', label='collection_1')
+      self.collection = Collection.objects.create2(name='collection_1', label='collection_1', owner=self.user)
     except:
       self.collection = Collection.objects.get(name='collection_1')
 

+ 30 - 25
apps/search/src/search/views.py

@@ -34,10 +34,9 @@ from indexer.management.commands import indexer_setup
 from search.api import _guess_gap, _zoom_range_facet, _new_range_facet
 from search.conf import SOLR_URL
 from search.data_export import download as export_download
-from search.decorators import allow_admin_only
+from search.decorators import allow_owner_only, allow_viewer_only
 from search.management.commands import search_setup
-from search.models import Collection, augment_solr_response, augment_solr_exception,\
-  pairwise2
+from search.models import Collection, augment_solr_response, augment_solr_exception, pairwise2
 from search.search_controller import SearchController
 
 
@@ -49,15 +48,12 @@ def index(request):
   collection_id = request.GET.get('collection')
 
   if not hue_collections or not collection_id:
-    if request.user.is_superuser:
-      return admin_collections(request, True)
-    else:
-      return no_collections(request)
+    return admin_collections(request, True)
 
   try:
-    collection = Collection.objects.get(id=collection_id) # TODO perms HUE-1987
+    collection = hue_collections.get(id=collection_id)
   except Exception, e:
-    raise PopupException(e, title=_('Error while accessing the collection'))
+    raise PopupException(e, title=_("Dashboard does not exist or you don't have the permission to access it."))
 
   query = {'qs': [{'q': ''}], 'fqs': [], 'start': 0}
 
@@ -65,10 +61,10 @@ def index(request):
     'collection': collection,
     'query': query,
     'initial': json.dumps({'collections': [], 'layout': []}),
+    'is_owner': request.user == collection.owner
   })
 
 
-@allow_admin_only
 def new_search(request):
   collections = SearchController(request.user).get_all_indexes()
   if not collections:
@@ -90,6 +86,7 @@ def new_search(request):
               "drops":["temp"],"klass":"card card-home card-column span10"}
          ]
      }),
+    'is_owner': True
   })
 
 
@@ -114,19 +111,17 @@ def browse(request, name):
               "drops":["temp"],"klass":"card card-home card-column span10"}
          ]
      }),
+     'is_owner': True
   })
 
 
+@allow_viewer_only
 def search(request):
   response = {}
 
   collection = json.loads(request.POST.get('collection', '{}'))
   query = json.loads(request.POST.get('query', '{}'))
   query['download'] = 'download' in request.POST
-  # todo: remove the selected histo facet if multiq
-
-  if collection['id']:
-    hue_collection = Collection.objects.get(id=collection['id']) # TODO perms
 
   if collection:
     try:
@@ -150,11 +145,11 @@ def search(request):
   return JsonResponse(response)
 
 
-@allow_admin_only
+@allow_owner_only
 def save(request):
   response = {'status': -1}
 
-  collection = json.loads(request.POST.get('collection', '{}')) # TODO perms
+  collection = json.loads(request.POST.get('collection', '{}'))
   layout = json.loads(request.POST.get('layout', '{}'))
 
   collection['template']['extracode'] = escape(collection['template']['extracode'])
@@ -163,7 +158,7 @@ def save(request):
     if collection['id']:
       hue_collection = Collection.objects.get(id=collection['id'])
     else:
-      hue_collection = Collection.objects.create2(name=collection['name'], label=collection['label'])
+      hue_collection = Collection.objects.create2(name=collection['name'], label=collection['label'], owner=request.user)
     hue_collection.update_properties({'collection': collection})
     hue_collection.update_properties({'layout': layout})
     hue_collection.name = collection['name']
@@ -179,6 +174,7 @@ def save(request):
   return JsonResponse(response)
 
 
+@allow_viewer_only
 def download(request):
   try:
     file_format = 'csv' if 'csv' in request.POST else 'xls' if 'xls' in request.POST else 'json'
@@ -200,9 +196,8 @@ def no_collections(request):
   return render('no_collections.mako', request, {})
 
 
-@allow_admin_only
 def admin_collections(request, is_redirect=False):
-  existing_hue_collections = Collection.objects.all()
+  existing_hue_collections = SearchController(request.user).get_search_collections()
 
   if request.GET.get('format') == 'json':
     collections = []
@@ -213,7 +208,9 @@ def admin_collections(request, is_redirect=False):
         'label': collection.label,
         'enabled': collection.enabled,
         'isCoreOnly': collection.is_core_only,
-        'absoluteUrl': collection.get_absolute_url()
+        'absoluteUrl': collection.get_absolute_url(),
+        'owner': collection.owner.username,
+        'isOwner': collection.owner == request.user or request.user.is_superuser
       }
       collections.append(massaged_collection)
     return JsonResponse(collections, safe=False)
@@ -224,7 +221,6 @@ def admin_collections(request, is_redirect=False):
   })
 
 
-@allow_admin_only
 def admin_collection_delete(request):
   if request.method != 'POST':
     raise PopupException(_('POST request required.'))
@@ -238,7 +234,7 @@ def admin_collection_delete(request):
   return JsonResponse(response)
 
 
-@allow_admin_only
+@allow_owner_only
 def admin_collection_copy(request):
   if request.method != 'POST':
     raise PopupException(_('POST request required.'))
@@ -275,7 +271,6 @@ def index_fields_dynamic(request):
 
   try:
     name = request.POST['name']
-
     hue_collection = Collection(name=name, label=name)
 
     dynamic_fields = SolrApi(SOLR_URL.get(), request.user).luke(hue_collection.name)
@@ -292,6 +287,7 @@ def index_fields_dynamic(request):
   return JsonResponse(result)
 
 
+@allow_viewer_only
 def get_document(request):
   result = {'status': -1, 'message': 'Error'}
 
@@ -317,6 +313,7 @@ def get_document(request):
   return JsonResponse(result)
 
 
+@allow_viewer_only
 def get_stats(request):
   result = {'status': -1, 'message': 'Error'}
 
@@ -341,6 +338,7 @@ def get_stats(request):
   return JsonResponse(result)
 
 
+@allow_viewer_only
 def get_terms(request):
   result = {'status': -1, 'message': 'Error'}
 
@@ -372,6 +370,7 @@ def get_terms(request):
   return JsonResponse(result)
 
 
+@allow_viewer_only
 def get_timeline(request):
   result = {'status': -1, 'message': 'Error'}
 
@@ -418,11 +417,12 @@ def get_timeline(request):
   return JsonResponse(result)
 
 
+@allow_viewer_only
 def new_facet(request):
   result = {'status': -1, 'message': 'Error'}
 
   try:
-    collection = json.loads(request.POST.get('collection', '{}')) # Perms
+    collection = json.loads(request.POST.get('collection', '{}'))
 
     facet_id = request.POST['id']
     facet_label = request.POST['label']
@@ -485,11 +485,13 @@ def _create_facet(collection, user, facet_id, facet_label, facet_field, widget_t
     'properties': properties
   }
 
+
+@allow_viewer_only
 def get_range_facet(request):
   result = {'status': -1, 'message': ''}
 
   try:
-    collection = json.loads(request.POST.get('collection', '{}')) # Perms
+    collection = json.loads(request.POST.get('collection', '{}'))
     facet = json.loads(request.POST.get('facet', '{}'))
     action = request.POST.get('action', 'select')
 
@@ -548,6 +550,9 @@ def get_collections(request):
 def install_examples(request):
   result = {'status': -1, 'message': ''}
 
+  if not request.user.is_superuser:
+    return PopupException(_("You must be a superuser."))
+
   if request.method != 'POST':
     result['message'] = _('A POST request is required.')
   else:

+ 7 - 2
desktop/core/src/desktop/templates/common_header.mako

@@ -446,7 +446,7 @@ from django.utils.translation import ugettext as _
        % endif
        % if 'search' in apps:
          <% from search.search_controller import SearchController %>
-         <% collections = SearchController(user).get_search_collections() %>
+         <% collections = SearchController(user).get_shared_search_collections() %>
          % if not collections:
            <li>
              <a title="${_('Solr Search')}" rel="navigator-tooltip" href="${ url('search:index') }">Search</a>
@@ -458,9 +458,14 @@ from django.utils.translation import ugettext as _
                % for collection in collections:
                  <li><a href="${ url('search:index') }?collection=${ collection.id }"><img src="${ static(collection.icon) }" class="app-icon"/> ${ collection.label }</a></li>
                % endfor
-               % if 'indexer' in apps:
+               % if 'indexer' in apps or 'search' in apps:
                  <li class="divider"></li>
+                 % if 'search' in apps:
+                 <li><a href="${ url('search:new_search') }"><i class="fa fa-plus"></i>${ _('Dashboard') }</a></li>
+                 % endif
+                 % if 'indexer' in apps:
                  <li><a href="${ url('indexer:collections') }"><i class="fa fa-database"></i> ${ _('Indexes') }</a></li>
+                 % endif
                % endif
              </ul>
            </li>