Skip to content

Commit

Permalink
Merge pull request getredash#1113 from whummer/feat/share-access-perm…
Browse files Browse the repository at this point in the history
…issions

Add: share modify/access permissions for queries and dashboard
  • Loading branch information
arikfr committed Oct 28, 2016
2 parents 7ad7d86 + d871ef8 commit fc3d6d1
Show file tree
Hide file tree
Showing 7 changed files with 420 additions and 36 deletions.
4 changes: 4 additions & 0 deletions handlers/api.py
Expand Up @@ -4,6 +4,7 @@

from redash.utils import json_dumps
from redash.handlers.base import org_scoped_rule
from redash.handlers.permissions import ObjectPermissionsListResource, CheckPermissionResource
from redash.handlers.alerts import AlertResource, AlertListResource, AlertSubscriptionListResource, AlertSubscriptionResource
from redash.handlers.dashboards import DashboardListResource, RecentDashboardsResource, DashboardResource, DashboardShareResource
from redash.handlers.data_sources import DataSourceTypeListResource, DataSourceListResource, DataSourceSchemaResource, DataSourceResource, DataSourcePauseResource, DataSourceTestResource
Expand Down Expand Up @@ -71,6 +72,9 @@ def json_representation(data, code, headers=None):
api.add_org_resource(QueryRefreshResource, '/api/queries/<query_id>/refresh', endpoint='query_refresh')
api.add_org_resource(QueryResource, '/api/queries/<query_id>', endpoint='query')

api.add_org_resource(ObjectPermissionsListResource, '/api/<object_type>/<object_id>/acl', endpoint='object_permissions')
api.add_org_resource(CheckPermissionResource, '/api/<object_type>/<object_id>/acl/<access_type>', endpoint='check_permissions')

api.add_org_resource(QueryResultListResource, '/api/query_results', endpoint='query_results')
api.add_org_resource(QueryResultResource,
'/api/query_results/<query_result_id>',
Expand Down
52 changes: 38 additions & 14 deletions handlers/dashboards.py
@@ -1,10 +1,12 @@
from flask import request, url_for
from flask_restful import abort

from funcy import distinct, take
from funcy import distinct, take, project
from itertools import chain

from redash import models
from redash.permissions import require_permission, require_admin_or_owner
from redash.models import ConflictDetectedError
from redash.permissions import require_permission, require_admin_or_owner, require_object_modify_permission, can_modify
from redash.handlers.base import BaseResource, get_object_or_404


Expand All @@ -24,18 +26,18 @@ class DashboardListResource(BaseResource):
@require_permission('list_dashboards')
def get(self):
dashboards = [d.to_dict() for d in models.Dashboard.all(self.current_org, self.current_user.groups, self.current_user)]

return dashboards

@require_permission('create_dashboard')
def post(self):
dashboard_properties = request.get_json(force=True)
dashboard = models.Dashboard(name=dashboard_properties['name'],
org=self.current_org,
user=self.current_user,
layout='[]')
dashboard.save()
return dashboard.to_dict()
dashboard = models.Dashboard.create(name=dashboard_properties['name'],
org=self.current_org,
user=self.current_user,
layout='[]')

result = dashboard.to_dict()
return result


class DashboardResource(BaseResource):
Expand All @@ -49,24 +51,34 @@ def get(self, dashboard_slug=None):
response['public_url'] = url_for('redash.public_dashboard', token=api_key.api_key, org_slug=self.current_org.slug, _external=True)
response['api_key'] = api_key.api_key

response['can_edit'] = can_modify(dashboard, self.current_user)

return response

@require_permission('edit_dashboard')
def post(self, dashboard_slug):
dashboard_properties = request.get_json(force=True)
# TODO: either convert all requests to use slugs or ids
dashboard = models.Dashboard.get_by_id_and_org(dashboard_slug, self.current_org)
dashboard.layout = dashboard_properties['layout']
dashboard.name = dashboard_properties['name']
dashboard.save()

return dashboard.to_dict(with_widgets=True, user=self.current_user)
require_object_modify_permission(dashboard, self.current_user)

updates = project(dashboard_properties, ('name', 'layout', 'version'))
updates['changed_by'] = self.current_user

try:
dashboard.update_instance(**updates)
except ConflictDetectedError:
abort(409)

result = dashboard.to_dict(with_widgets=True, user=self.current_user)
return result

@require_permission('edit_dashboard')
def delete(self, dashboard_slug):
dashboard = models.Dashboard.get_by_slug_and_org(dashboard_slug, self.current_org)
dashboard.is_archived = True
dashboard.save()
dashboard.save(changed_by=self.current_user)

return dashboard.to_dict(with_widgets=True, user=self.current_user)

Expand All @@ -78,6 +90,12 @@ def post(self, dashboard_id):
api_key = models.ApiKey.create_for_object(dashboard, self.current_user)
public_url = url_for('redash.public_dashboard', token=api_key.api_key, org_slug=self.current_org.slug, _external=True)

self.record_event({
'action': 'activate_api_key',
'object_id': dashboard.id,
'object_type': 'dashboard',
})

return {'public_url': public_url, 'api_key': api_key.api_key}

def delete(self, dashboard_id):
Expand All @@ -89,4 +107,10 @@ def delete(self, dashboard_id):
api_key.active = False
api_key.save()

self.record_event({
'action': 'deactivate_api_key',
'object_id': dashboard.id,
'object_type': 'dashboard',
})


96 changes: 96 additions & 0 deletions handlers/permissions.py
@@ -0,0 +1,96 @@
from collections import defaultdict

from redash.handlers.base import BaseResource, get_object_or_404
from redash.models import AccessPermission, Query, Dashboard, User
from redash.permissions import require_admin_or_owner, ACCESS_TYPES
from flask import request
from flask_restful import abort


model_to_types = {
'queries': Query,
'dashboards': Dashboard
}


def get_model_from_type(type):
model = model_to_types.get(type)
if model is None:
abort(404)
return model


class ObjectPermissionsListResource(BaseResource):
def get(self, object_type, object_id):
model = get_model_from_type(object_type)
obj = get_object_or_404(model.get_by_id_and_org, object_id, self.current_org)

# TODO: include grantees in search to avoid N+1 queries
permissions = AccessPermission.find(obj)

result = defaultdict(list)

for perm in permissions:
result[perm.access_type].append(perm.grantee.to_dict())

return result

def post(self, object_type, object_id):
model = get_model_from_type(object_type)
obj = get_object_or_404(model.get_by_id_and_org, object_id, self.current_org)

require_admin_or_owner(obj.user_id)

req = request.get_json(True)

access_type = req['access_type']

if access_type not in ACCESS_TYPES:
abort(400, message='Unknown access type.')

try:
grantee = User.get_by_id_and_org(req['user_id'], self.current_org)
except User.DoesNotExist:
abort(400, message='User not found.')

permission = AccessPermission.grant(obj, access_type, grantee, self.current_user)

self.record_event({
'action': 'grant_permission',
'object_id': object_id,
'object_type': object_type,
'access_type': access_type,
'grantee': grantee.id
})

return permission.to_dict()

def delete(self, object_type, object_id):
model = get_model_from_type(object_type)
obj = get_object_or_404(model.get_by_id_and_org, object_id, self.current_org)

require_admin_or_owner(obj.user_id)

req = request.get_json(True)
grantee = req['user_id']
access_type = req['access_type']

AccessPermission.revoke(obj, grantee, access_type)

self.record_event({
'action': 'revoke_permission',
'object_id': object_id,
'object_type': object_type,
'access_type': access_type,
'grantee': grantee
})


class CheckPermissionResource(BaseResource):
def get(self, object_type, object_id, access_type):
model = get_model_from_type(object_type)
obj = get_object_or_404(model.get_by_id_and_org, object_id, self.current_org)

has_access = AccessPermission.exists(obj, access_type, self.current_user)

return {'response': has_access}
30 changes: 20 additions & 10 deletions handlers/queries.py
Expand Up @@ -9,7 +9,8 @@
from redash.handlers.base import routes, org_scoped_rule, paginate
from redash.handlers.query_results import run_query
from redash import models
from redash.permissions import require_permission, require_access, require_admin_or_owner, not_view_only, view_only
from redash.permissions import require_permission, require_access, require_admin_or_owner, not_view_only, view_only, \
require_object_modify_permission, can_modify
from redash.handlers.base import BaseResource, get_object_or_404
from redash.utils import collect_parameters_from_request

Expand Down Expand Up @@ -93,9 +94,10 @@ class QueryResource(BaseResource):
@require_permission('edit_query')
def post(self, query_id):
query = get_object_or_404(models.Query.get_by_id_and_org, query_id, self.current_org)
require_admin_or_owner(query.user_id)

query_def = request.get_json(force=True)

require_object_modify_permission(query, self.current_user)

for field in ['id', 'created_at', 'api_key', 'visualizations', 'latest_query_data', 'user', 'last_modified_by', 'org']:
query_def.pop(field, None)

Expand All @@ -106,26 +108,34 @@ def post(self, query_id):
query_def['data_source'] = query_def.pop('data_source_id')

query_def['last_modified_by'] = self.current_user
query_def['changed_by'] = self.current_user

try:
query.update_instance(**query_def)
except models.ConflictDetectedError:
abort(409)

query.update_instance(**query_def)
# old_query = copy.deepcopy(query.to_dict())
# new_change = query.update_instance_tracked(changing_user=self.current_user, old_object=old_query, **query_def)
# abort(409) # HTTP 'Conflict' status code

return query.to_dict(with_visualizations=True)
result = query.to_dict(with_visualizations=True)
return result

@require_permission('view_query')
def get(self, query_id):
q = get_object_or_404(models.Query.get_by_id_and_org, query_id, self.current_org)
require_access(q.groups, self.current_user, view_only)

if q:
return q.to_dict(with_visualizations=True)
else:
abort(404, message="Query not found.")
result = q.to_dict(with_visualizations=True)
result['can_edit'] = can_modify(q, self.current_user)
return result

# TODO: move to resource of its own? (POST /queries/{id}/archive)
def delete(self, query_id):
query = get_object_or_404(models.Query.get_by_id_and_org, query_id, self.current_org)
require_admin_or_owner(query.user_id)
query.archive()
query.archive(self.current_user)


class QueryRefreshResource(BaseResource):
Expand Down

0 comments on commit fc3d6d1

Please sign in to comment.