diff --git a/client/app/pages/dashboards/ShareDashboardDialog.jsx b/client/app/pages/dashboards/ShareDashboardDialog.jsx index 96ee577f3a..be555af857 100644 --- a/client/app/pages/dashboards/ShareDashboardDialog.jsx +++ b/client/app/pages/dashboards/ShareDashboardDialog.jsx @@ -16,7 +16,7 @@ const API_SHARE_URL = 'api/dashboards/{id}/share'; class ShareDashboardDialog extends React.Component { static propTypes = { dashboard: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types - hasQueryParams: PropTypes.bool.isRequired, + hasOnlySafeQueries: PropTypes.bool.isRequired, dialog: DialogPropType.isRequired, }; @@ -35,7 +35,7 @@ class ShareDashboardDialog extends React.Component { }; this.apiUrl = replace(API_SHARE_URL, '{id}', dashboard.id); - this.disabled = this.props.hasQueryParams && !dashboard.publicAccessEnabled; + this.enabled = this.props.hasOnlySafeQueries || dashboard.publicAccessEnabled; } static get headerContent() { @@ -104,10 +104,10 @@ class ShareDashboardDialog extends React.Component { footer={null} >
- {this.props.hasQueryParams && ( + {!this.props.hasOnlySafeQueries && ( @@ -117,12 +117,13 @@ class ShareDashboardDialog extends React.Component { checked={dashboard.publicAccessEnabled} onChange={this.onChange} loading={this.state.saving} - disabled={this.disabled} + disabled={!this.enabled} + data-test="PublicAccessEnabled" /> {dashboard.public_url && ( - + )}
diff --git a/client/app/pages/dashboards/dashboard.html b/client/app/pages/dashboards/dashboard.html index a52bb540c7..51aedb5a6a 100644 --- a/client/app/pages/dashboards/dashboard.html +++ b/client/app/pages/dashboards/dashboard.html @@ -67,7 +67,7 @@

- diff --git a/client/app/pages/dashboards/dashboard.js b/client/app/pages/dashboards/dashboard.js index e1303c3301..3d06c21419 100644 --- a/client/app/pages/dashboards/dashboard.js +++ b/client/app/pages/dashboards/dashboard.js @@ -387,15 +387,14 @@ function DashboardCtrl( } this.openShareForm = () => { - // check if any of the wigets have query parameters - const hasQueryParams = _.some( + const hasOnlySafeQueries = _.every( this.dashboard.widgets, - w => !_.isEmpty(w.getQuery() && w.getQuery().getParametersDefs()), + w => (w.getQuery() ? w.getQuery().is_safe : true), ); ShareDashboardDialog.showModal({ dashboard: this.dashboard, - hasQueryParams, + hasOnlySafeQueries, }); }; } diff --git a/client/app/pages/dashboards/public-dashboard-page.html b/client/app/pages/dashboards/public-dashboard-page.html index 9c04cdcf06..6dd8343447 100644 --- a/client/app/pages/dashboards/public-dashboard-page.html +++ b/client/app/pages/dashboards/public-dashboard-page.html @@ -1,6 +1,10 @@ -
+
+
+ +
+
diff --git a/client/app/pages/dashboards/public-dashboard-page.js b/client/app/pages/dashboards/public-dashboard-page.js index 58047614fa..706b058b67 100644 --- a/client/app/pages/dashboards/public-dashboard-page.js +++ b/client/app/pages/dashboards/public-dashboard-page.js @@ -25,41 +25,47 @@ const PublicDashboardPage = { this.logoUrl = logoUrl; this.public = true; - this.dashboard.widgets = Dashboard.prepareDashboardWidgets(this.dashboard.widgets); + this.globalParameters = []; + + this.extractGlobalParameters = () => { + this.globalParameters = this.dashboard.getParametersDefs(); + }; const refreshRate = Math.max(30, parseFloat($location.search().refresh)); - if (refreshRate) { - const refresh = () => { - loadDashboard($http, $route).then((data) => { - this.dashboard = data; - this.dashboard.widgets = Dashboard.prepareDashboardWidgets(this.dashboard.widgets); - this.dashboard.widgets.forEach(widget => widget.load()); + this.refreshDashboard = () => { + loadDashboard($http, $route).then((data) => { + this.dashboard = new Dashboard(data); + this.dashboard.widgets = Dashboard.prepareDashboardWidgets(this.dashboard.widgets); + this.dashboard.widgets.forEach((widget) => { + widget.load(!!refreshRate).catch((error) => { + const isSafe = widget.getQuery() ? widget.getQuery().is_safe : true; + if (!isSafe) { + error.errorMessage = 'This query contains potentially unsafe parameters and cannot be executed on a publicly shared dashboard.'; + } + }); + }); + this.filters = []; // TODO: implement (@/services/dashboard.js:collectDashboardFilters) + this.filtersOnChange = (allFilters) => { + this.filters = allFilters; + $scope.$applyAsync(); + }; - this.filters = []; // TODO: implement (@/services/dashboard.js:collectDashboardFilters) - this.filtersOnChange = (allFilters) => { - this.filters = allFilters; - $scope.$applyAsync(); - }; + this.extractGlobalParameters(); + }); - $timeout(refresh, refreshRate * 1000.0); - }); - }; + if (refreshRate) { + $timeout(this.refreshDashboard, refreshRate * 1000.0); + } + }; - $timeout(refresh, refreshRate * 1000.0); - } + this.refreshDashboard(); }, }; export default function init(ngModule) { ngModule.component('publicDashboardPage', PublicDashboardPage); - function loadPublicDashboard($http, $route) { - 'ngInject'; - - return loadDashboard($http, $route); - } - function session($http, $route, Auth) { const token = $route.current.params.token; Auth.setApiKey(token); @@ -68,10 +74,9 @@ export default function init(ngModule) { ngModule.config(($routeProvider) => { $routeProvider.when('/public/dashboards/:token', { - template: '', + template: '', reloadOnSearch: false, resolve: { - dashboard: loadPublicDashboard, session, }, }); diff --git a/client/app/services/dashboard.js b/client/app/services/dashboard.js index 69896441ac..398f96cf47 100644 --- a/client/app/services/dashboard.js +++ b/client/app/services/dashboard.js @@ -183,7 +183,6 @@ function DashboardService($resource, $http, $location, currentUser) { resource.prepareDashboardWidgets = prepareDashboardWidgets; resource.prepareWidgetsForDashboard = prepareWidgetsForDashboard; - resource.prototype.getParametersDefs = function getParametersDefs() { const globalParams = {}; const queryParams = $location.search(); @@ -192,7 +191,7 @@ function DashboardService($resource, $http, $location, currentUser) { const mappings = widget.getParameterMappings(); widget .getQuery() - .getParametersDefs() + .getParametersDefs(false) .forEach((param) => { const mapping = mappings[param.name]; if (mapping.type === Widget.MappingType.DashboardLevel) { diff --git a/client/app/services/query.js b/client/app/services/query.js index 8c00829ca1..65161d9c22 100644 --- a/client/app/services/query.js +++ b/client/app/services/query.js @@ -219,25 +219,32 @@ class Parameters { } parseQuery() { + const fallback = () => map(this.query.options.parameters, i => i.name); + let parameters = []; - try { - const parts = Mustache.parse(this.query.query); - parameters = uniq(collectParams(parts)); - } catch (e) { - logger('Failed parsing parameters: ', e); - // Return current parameters so we don't reset the list - parameters = map(this.query.options.parameters, i => i.name); + if (this.query.query) { + try { + const parts = Mustache.parse(this.query.query); + parameters = uniq(collectParams(parts)); + } catch (e) { + logger('Failed parsing parameters: ', e); + // Return current parameters so we don't reset the list + parameters = fallback(); + } + } else { + parameters = fallback(); } + return parameters; } - updateParameters() { - if (this.query.query === this.cachedQueryText) { + updateParameters(update) { + if (this.query.query && this.query.query === this.cachedQueryText) { return; } this.cachedQueryText = this.query.query; - const parameterNames = this.parseQuery(); + const parameterNames = update ? this.parseQuery() : map(this.query.options.parameters, p => p.name); this.query.options.parameters = this.query.options.parameters || []; @@ -269,8 +276,8 @@ class Parameters { }); } - get() { - this.updateParameters(); + get(update = true) { + this.updateParameters(update); return this.query.options.parameters; } @@ -478,10 +485,6 @@ function QueryResource( }; QueryService.prototype.prepareQueryResultExecution = function prepareQueryResultExecution(execute, maxAge) { - if (!this.query) { - return new QueryResultError("Can't execute empty query."); - } - const parameters = this.getParameters(); const missingParams = parameters.getMissing(); @@ -517,10 +520,8 @@ function QueryResource( if (!this.queryResult) { this.queryResult = QueryResult.getById(this.id, this.latest_query_data_id); } - } else if (this.data_source_id) { - this.queryResult = execute(); } else { - return new QueryResultError('Please select data source to run this query.'); + this.queryResult = execute(); } return this.queryResult; @@ -533,6 +534,10 @@ function QueryResource( QueryService.prototype.getQueryResultByText = function getQueryResultByText(maxAge, selectedQueryText) { const queryText = selectedQueryText || this.query; + if (!queryText) { + return new QueryResultError("Can't execute empty query."); + } + const parameters = this.getParameters().getValues(); const execute = () => QueryResult.get(this.data_source_id, queryText, parameters, maxAge, this.id); return this.prepareQueryResultExecution(execute, maxAge); @@ -576,8 +581,8 @@ function QueryResource( return this.$parameters; }; - QueryService.prototype.getParametersDefs = function getParametersDefs() { - return this.getParameters().get(); + QueryService.prototype.getParametersDefs = function getParametersDefs(update = true) { + return this.getParameters().get(update); }; return QueryService; diff --git a/client/app/services/widget.js b/client/app/services/widget.js index ecc8b36300..a682879c62 100644 --- a/client/app/services/widget.js +++ b/client/app/services/widget.js @@ -212,7 +212,7 @@ function WidgetFactory($http, $location, Query) { const existingParams = {}; // textboxes does not have query - const params = this.getQuery() ? this.getQuery().getParametersDefs() : []; + const params = this.getQuery() ? this.getQuery().getParametersDefs(false) : []; each(params, (param) => { existingParams[param.name] = true; if (!isObject(this.options.parameterMappings[param.name])) { diff --git a/client/cypress/integration/dashboard/dashboard_spec.js b/client/cypress/integration/dashboard/dashboard_spec.js index 2d966432b5..2a21b5f3ac 100644 --- a/client/cypress/integration/dashboard/dashboard_spec.js +++ b/client/cypress/integration/dashboard/dashboard_spec.js @@ -109,6 +109,154 @@ describe('Dashboard', () => { }); }); + describe('Sharing', () => { + beforeEach(function () { + createDashboard('Foo Bar').then(({ slug, id }) => { + this.dashboardId = id; + this.dashboardUrl = `/dashboard/${slug}`; + }); + }); + + it('is possible if all queries are safe', function () { + const options = { + parameters: [{ + name: 'foo', + type: 'number', + }], + }; + + const dashboardUrl = this.dashboardUrl; + createQuery({ options }).then(({ id: queryId }) => { + cy.visit(dashboardUrl); + editDashboard(); + cy.contains('a', 'Add Widget').click(); + cy.getByTestId('AddWidgetDialog').within(() => { + cy.get(`.query-selector-result[data-test="QueryId${queryId}"]`).click(); + }); + cy.contains('button', 'Add to Dashboard').click(); + cy.getByTestId('AddWidgetDialog').should('not.exist'); + cy.clickThrough({ button: ` + Done Editing + Publish + ` }, + `OpenShareForm + PublicAccessEnabled`); + + cy.getByTestId('SecretAddress').should('exist'); + }); + }); + + describe('is available to unauthenticated users', () => { + const addWidgetAndShareDashboard = (dashboardUrl, query, options, callback) => { + createQuery({ query, options }).then(({ id: queryId }) => { + cy.visit(dashboardUrl); + editDashboard(); + cy.contains('a', 'Add Widget').click(); + cy.getByTestId('AddWidgetDialog').within(() => { + cy.get(`.query-selector-result[data-test="QueryId${queryId}"]`).click(); + }); + cy.contains('button', 'Add to Dashboard').click(); + cy.getByTestId('AddWidgetDialog').should('not.exist'); + cy.clickThrough({ button: ` + Done Editing + Publish + ` }, + `OpenShareForm + PublicAccessEnabled`); + + cy.getByTestId('SecretAddress').invoke('val').then((secretAddress) => { + callback(secretAddress); + }); + }); + }; + + it.only('when there are no parameters', function () { + addWidgetAndShareDashboard(this.dashboardUrl, 'select 1', {}, (secretAddress) => { + cy.logout(); + cy.visit(secretAddress); + cy.getByTestId('DynamicTable', { timeout: 10000 }).should('exist'); + cy.percySnapshot('Successfully Shared Unparameterized Dashboard'); + }); + }); + + it('when there are only safe parameters', function () { + addWidgetAndShareDashboard(this.dashboardUrl, "select '{{foo}}'", { + parameters: [{ + name: 'foo', + type: 'number', + value: 1, + }], + }, (secretAddress) => { + cy.logout(); + cy.visit(secretAddress); + cy.getByTestId('DynamicTable', { timeout: 10000 }).should('exist'); + cy.percySnapshot('Successfully Shared Parameterized Dashboard'); + }); + }); + + it('even when there are suddenly some unsafe parameters', function () { + // start out by creating a dashboard with no parameters & share it + const dashboardUrl = this.dashboardUrl; + addWidgetAndShareDashboard(dashboardUrl, 'select 1', {}, (secretAddress) => { + // then, after it is shared, add an unsafe parameterized query to it + createQuery({ + query: "select '{{foo}}'", + options: { + parameters: [{ + name: 'foo', + type: 'text', + value: 'oh snap!', + }], + }, + }).then(({ id: queryId }) => { + cy.visit(dashboardUrl); + editDashboard(); + cy.contains('a', 'Add Widget').click(); + cy.getByTestId('AddWidgetDialog').within(() => { + cy.get(`.query-selector-result[data-test="QueryId${queryId}"]`).click(); + }); + cy.contains('button', 'Add to Dashboard').click(); + cy.getByTestId('AddWidgetDialog').should('not.exist'); + cy.contains('button', 'Done Editing').click(); + cy.logout(); + cy.visit(secretAddress); + cy.getByTestId('DynamicTable', { timeout: 10000 }).should('exist'); + cy.percySnapshot('Successfully Shared Parameterized Dashboard With Some Unsafe Queries'); + }); + }); + }); + }); + + it('is not possible if some queries are not safe', function () { + const options = { + parameters: [{ + name: 'foo', + type: 'text', + }], + }; + + const dashboardUrl = this.dashboardUrl; + createQuery({ options }).then(({ id: queryId }) => { + cy.visit(dashboardUrl); + editDashboard(); + cy.contains('a', 'Add Widget').click(); + cy.getByTestId('AddWidgetDialog').within(() => { + cy.get(`.query-selector-result[data-test="QueryId${queryId}"]`).click(); + }); + cy.contains('button', 'Add to Dashboard').click(); + cy.getByTestId('AddWidgetDialog').should('not.exist'); + cy.clickThrough({ button: ` + Done Editing + Publish + ` }, + 'OpenShareForm'); + + cy.getByTestId('PublicAccessEnabled').should('be.disabled'); + }); + }); + }); + + describe('Textbox', () => { beforeEach(function () { createDashboard('Foo Bar').then(({ slug, id }) => { @@ -425,6 +573,13 @@ describe('Dashboard', () => { const paramName = 'count'; const queryData = { query: `select s.a FROM generate_series(1,{{ ${paramName} }}) AS s(a)`, + options: { + parameters: [{ + title: paramName, + name: paramName, + type: 'text', + }], + }, }; beforeEach(function () { diff --git a/client/cypress/integration/query/parameter_spec.js b/client/cypress/integration/query/parameter_spec.js index 87190b7855..dd8df75ea8 100644 --- a/client/cypress/integration/query/parameter_spec.js +++ b/client/cypress/integration/query/parameter_spec.js @@ -131,7 +131,7 @@ describe('Parameter', () => { ParameterTypeSelect DateParameterTypeOption UseCurrentDateTimeCheckbox - SaveParameterSettings + SaveParameterSettings `); const now = new Date(); diff --git a/client/cypress/support/commands.js b/client/cypress/support/commands.js index 97efba27f2..37654fccdf 100644 --- a/client/cypress/support/commands.js +++ b/client/cypress/support/commands.js @@ -16,12 +16,32 @@ Cypress.Commands.add('login', (email = 'admin@redash.io', password = 'password') Cypress.Commands.add('logout', () => cy.request('/logout')); Cypress.Commands.add('getByTestId', element => cy.get('[data-test="' + element + '"]')); -Cypress.Commands.add('clickThrough', (elements) => { - elements - .trim() - .split(/\s/) - .filter(Boolean) - .forEach(element => cy.getByTestId(element).click()); + +/* Clicks a series of elements. Pass in a newline-seperated string in order to click all elements by their test id, + or enclose the above string in an object with 'button' as key to click the buttons by name. For example: + + cy.clickThrough(` + TestId1 + TestId2 + TestId3 + `, { button: ` + Label of button 4 + Label of button 5 + ` }, ` + TestId6 + TestId7`); +*/ +Cypress.Commands.add('clickThrough', (...args) => { + args.forEach((elements) => { + const names = elements.button || elements; + + const click = element => (elements.button ? + cy.contains('button', element.trim()) : + cy.getByTestId(element.trim())).click(); + + names.trim().split(/\n/).filter(Boolean).forEach(click); + }); + return undefined; }); diff --git a/redash/handlers/authentication.py b/redash/handlers/authentication.py index 461e033e5a..6c002106c2 100644 --- a/redash/handlers/authentication.py +++ b/redash/handlers/authentication.py @@ -259,6 +259,18 @@ def messages(): return messages +def messages(): + messages = [] + + if not current_user.is_email_verified: + messages.append('email-not-verified') + + if settings.ALLOW_PARAMETERS_IN_EMBEDS: + messages.append('using-deprecated-embed-feature') + + return messages + + @routes.route('/api/config', methods=['GET']) def config(org_slug=None): return json_response({ diff --git a/redash/handlers/queries.py b/redash/handlers/queries.py index ec890b2558..c9a48f38dc 100644 --- a/redash/handlers/queries.py +++ b/redash/handlers/queries.py @@ -186,6 +186,7 @@ def require_access_to_dropdown_queries(user, query_def): require_access(dict(groups), user, view_only) + class QueryListResource(BaseQueryListResource): @require_permission('create_query') def post(self): diff --git a/redash/handlers/query_results.py b/redash/handlers/query_results.py index 0899c0e7a5..815a7a154d 100644 --- a/redash/handlers/query_results.py +++ b/redash/handlers/query_results.py @@ -88,7 +88,11 @@ def post(self): parameterized_query = ParameterizedQuery(query) - data_source = models.DataSource.get_by_id_and_org(params.get('data_source_id'), self.current_org) + data_source_id = params.get('data_source_id') + if data_source_id: + data_source = models.DataSource.get_by_id_and_org(params.get('data_source_id'), self.current_org) + else: + return {'job': {'status': 4, 'error': 'Please select data source to run this query.'}}, 401 if not has_access(data_source, self.current_user, not_view_only): return {'job': {'status': 4, 'error': 'You do not have permission to run queries with this data source.'}}, 403 diff --git a/redash/models/__init__.py b/redash/models/__init__.py index 5cecdbc530..df1bae38d9 100644 --- a/redash/models/__init__.py +++ b/redash/models/__init__.py @@ -690,6 +690,20 @@ def parameters(self): def parameterized(self): return ParameterizedQuery(self.query_text, self.parameters) + @property + def dashboard_api_keys(self): + query = """SELECT api_keys.api_key + FROM api_keys + JOIN dashboards ON object_id = dashboards.id + JOIN widgets ON dashboards.id = widgets.dashboard_id + JOIN visualizations ON widgets.visualization_id = visualizations.id + WHERE object_type='dashboards' + AND active=true + AND visualizations.query_id = :id""" + + api_keys = db.session.execute(query, {'id': self.id}).fetchall() + return [api_key[0] for api_key in api_keys] + @listens_for(Query.query_text, 'set') def gen_query_hash(target, val, oldval, initiator): diff --git a/redash/permissions.py b/redash/permissions.py index 9de65efd55..d928d918c9 100644 --- a/redash/permissions.py +++ b/redash/permissions.py @@ -16,13 +16,19 @@ def has_access(obj, user, need_view_only): if hasattr(obj, 'api_key') and user.is_api_user(): - return has_access_to_object(obj, user, need_view_only) + return has_access_to_object(obj, user.id, need_view_only) else: return has_access_to_groups(obj, user, need_view_only) -def has_access_to_object(obj, user, need_view_only): - return (obj.api_key == user.id) and need_view_only +def has_access_to_object(obj, api_key, need_view_only): + if obj.api_key == api_key: + return need_view_only + elif hasattr(obj, 'dashboard_api_keys'): + # check if api_key belongs to a dashboard containing this query + return api_key in obj.dashboard_api_keys and need_view_only + else: + return False def has_access_to_groups(obj, user, need_view_only): diff --git a/redash/serializers/__init__.py b/redash/serializers/__init__.py index 605a3baa9a..ba51ad0eb1 100644 --- a/redash/serializers/__init__.py +++ b/redash/serializers/__init__.py @@ -25,21 +25,20 @@ def public_widget(widget): 'created_at': widget.created_at } - if widget.visualization and widget.visualization.id: - query_data = models.QueryResult.query.get(widget.visualization.query_rel.latest_query_data_id).to_dict() + v = widget.visualization + if v and v.id: res['visualization'] = { - 'type': widget.visualization.type, - 'name': widget.visualization.name, - 'description': widget.visualization.description, - 'options': json_loads(widget.visualization.options), - 'updated_at': widget.visualization.updated_at, - 'created_at': widget.visualization.created_at, + 'type': v.type, + 'name': v.name, + 'description': v.description, + 'options': json_loads(v.options), + 'updated_at': v.updated_at, + 'created_at': v.created_at, 'query': { - 'query': ' ', # workaround, as otherwise the query data won't be loaded. - 'name': widget.visualization.query_rel.name, - 'description': widget.visualization.query_rel.description, - 'options': {}, - 'latest_query_data': query_data + 'id': v.query_rel.id, + 'name': v.query_rel.name, + 'description': v.query_rel.description, + 'options': v.query_rel.options } } diff --git a/redash/tasks/queries.py b/redash/tasks/queries.py index f322b51052..b54c831b55 100644 --- a/redash/tasks/queries.py +++ b/redash/tasks/queries.py @@ -289,11 +289,15 @@ class QueryExecutionError(Exception): pass -def _resolve_user(user_id, is_api_key): +def _resolve_user(user_id, is_api_key, query_id): if user_id is not None: if is_api_key: api_key = user_id - q = models.Query.by_api_key(api_key) + if query_id is not None: + q = models.Query.get_by_id(query_id) + else: + q = models.Query.by_api_key(api_key) + return models.ApiUser(api_key, q.org, q.groups) else: return models.User.get_by_id(user_id) @@ -311,7 +315,7 @@ def __init__(self, task, query, data_source_id, user_id, is_api_key, metadata, self.data_source_id = data_source_id self.metadata = metadata self.data_source = self._load_data_source() - self.user = _resolve_user(user_id, is_api_key) + self.user = _resolve_user(user_id, is_api_key, metadata.get('Query ID')) # Close DB connection to prevent holding a connection for a long time while the query is executing. models.db.session.close() diff --git a/tests/handlers/test_query_results.py b/tests/handlers/test_query_results.py index 8d9add7ec1..6bf8815bf4 100644 --- a/tests/handlers/test_query_results.py +++ b/tests/handlers/test_query_results.py @@ -105,6 +105,15 @@ def test_execute_on_paused_data_source(self): self.assertNotIn('query_result', rv.json) self.assertIn('job', rv.json) + def test_execute_without_data_source(self): + rv = self.make_request('post', '/api/query_results', + data={'query': 'SELECT 1', + 'max_age': 0}) + + self.assertEquals(rv.status_code, 401) + self.assertNotIn('query_result', rv.json) + self.assertIn('job', rv.json) + class TestQueryResultAPI(BaseTestCase): def test_has_no_access_to_data_source(self): diff --git a/tests/test_permissions.py b/tests/test_permissions.py index b2a65076c4..11cffd1be4 100644 --- a/tests/test_permissions.py +++ b/tests/test_permissions.py @@ -1,13 +1,15 @@ +from tests import BaseTestCase from collections import namedtuple from unittest import TestCase from redash.permissions import has_access +from redash import models MockUser = namedtuple('MockUser', ['permissions', 'group_ids']) view_only = True -class TestHasAccess(TestCase): +class TestHasAccess(BaseTestCase): def test_allows_admin_regardless_of_groups(self): user = MockUser(['admin'], []) @@ -39,3 +41,27 @@ def test_not_allows_if_not_enough_permission(self): self.assertFalse(has_access({2: view_only}, user, not view_only)) self.assertFalse(has_access({2: view_only}, user, view_only)) self.assertFalse(has_access({2: not view_only, 1: view_only}, user, not view_only)) + + def test_allows_access_to_query_by_query_api_key(self): + query = self.factory.create_query() + user = models.ApiUser(query.api_key, None, []) + + self.assertTrue(has_access(query, user, view_only)) + + def test_doesnt_allow_access_to_query_by_different_api_key(self): + query = self.factory.create_query() + other_query = self.factory.create_query() + user = models.ApiUser(other_query.api_key, None, []) + + self.assertFalse(has_access(query, user, view_only)) + + def test_allows_access_to_query_by_dashboard_api_key(self): + dashboard = self.factory.create_dashboard() + visualization = self.factory.create_visualization() + self.factory.create_widget(dashboard=dashboard, visualization=visualization) + query = self.factory.create_query(visualizations=[visualization]) + + api_key = self.factory.create_api_key(object=dashboard).api_key + user = models.ApiUser(api_key, None, []) + + self.assertTrue(has_access(query, user, view_only)) \ No newline at end of file