Skip to content

Commit

Permalink
Support regenerating Query API Key (getredash#3764)
Browse files Browse the repository at this point in the history
* Add regenerate function of query's API Key

* Add regenerate API Key button

* Add regenerate Query API Key tests

* Fix too long line

* Replace  with this

* Return a simple version query

* Update only API Key

* Update API Key via query
  • Loading branch information
kyoshidajp authored and harveyrendell committed Nov 14, 2019
1 parent b5aaa87 commit 2e1c70e
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 8 deletions.
34 changes: 27 additions & 7 deletions client/app/components/queries/api-key-dialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,46 @@ const ApiKeyDialog = {
</div>
<div class="modal-body">
<h5>API Key</h5>
<pre>{{$ctrl.apiKey}}</pre>
<div class="form-group">
<pre>{{$ctrl.query.api_key}}</pre>
<div ng-if="$ctrl.canEdit">
<button class="btn btn-default" ng-click="$ctrl.regenerateQueryApiKey()" ng-disabled="$ctrl.disableRegenerateApiKeyButton">Regenerate</button>
</div>
</div>
<h5>Example API Calls:</h5>
<div>
Results in CSV format:
<pre>{{$ctrl.csvUrl}}</pre>
<pre>{{$ctrl.csvUrlBase + $ctrl.query.api_key}}</pre>
Results in JSON format:
<pre>{{$ctrl.jsonUrl}}</pre>
<pre>{{$ctrl.jsonUrlBase + $ctrl.query.api_key}}</pre>
</div>
</div>`,
controller(clientConfig) {
controller($http, clientConfig, currentUser) {
'ngInject';

this.apiKey = this.resolve.query.api_key;
this.csvUrl = `${clientConfig.basePath}api/queries/${this.resolve.query.id}/results.csv?api_key=${this.apiKey}`;
this.jsonUrl = `${clientConfig.basePath}api/queries/${this.resolve.query.id}/results.json?api_key=${this.apiKey}`;
this.canEdit = currentUser.id === this.resolve.query.user.id || currentUser.hasPermission('admin');
this.disableRegenerateApiKeyButton = false;
this.query = this.resolve.query;
this.csvUrlBase = `${clientConfig.basePath}api/queries/${this.resolve.query.id}/results.csv?api_key=`;
this.jsonUrlBase = `${clientConfig.basePath}api/queries/${this.resolve.query.id}/results.json?api_key=`;

this.regenerateQueryApiKey = () => {
this.disableRegenerateApiKeyButton = true;
$http
.post(`api/queries/${this.resolve.query.id}/regenerate_api_key`)
.success((data) => {
this.query.api_key = data.api_key;
this.disableRegenerateApiKeyButton = false;
})
.error(() => {
this.disableRegenerateApiKeyButton = false;
});
};
},
bindings: {
resolve: '<',
Expand Down
6 changes: 5 additions & 1 deletion redash/handlers/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@
QueryForkResource, QueryListResource,
QueryRecentResource, QueryRefreshResource,
QueryResource, QuerySearchResource,
QueryTagsResource)
QueryTagsResource,
QueryRegenerateApiKeyResource)
from redash.handlers.query_results import (JobResource,
QueryResultDropdownResource,
QueryDropdownsResource,
Expand Down Expand Up @@ -115,6 +116,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(QueryForkResource, '/api/queries/<query_id>/fork', endpoint='query_fork')
api.add_org_resource(QueryRegenerateApiKeyResource,
'/api/queries/<query_id>/regenerate_api_key',
endpoint='query_regenerate_api_key')

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')
Expand Down
18 changes: 18 additions & 0 deletions redash/handlers/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,24 @@ def delete(self, query_id):
models.db.session.commit()


class QueryRegenerateApiKeyResource(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.regenerate_api_key()
models.db.session.commit()

self.record_event({
'action': 'regnerate_api_key',
'object_id': query_id,
'object_type': 'query',
})

result = QuerySerializer(query).serialize()
return result


class QueryForkResource(BaseResource):
@require_permission('edit_query')
def post(self, query_id):
Expand Down
3 changes: 3 additions & 0 deletions redash/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,9 @@ def archive(self, user=None):
if user:
self.record_changes(user)

def regenerate_api_key(self):
self.api_key = generate_token(40)

@classmethod
def create(cls, **kwargs):
query = cls(**kwargs)
Expand Down
49 changes: 49 additions & 0 deletions tests/handlers/test_queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,55 @@ def test_refresh_forbiden_with_query_api_key(self):
self.assertEqual(200, response.status_code)


class TestQueryRegenerateApiKey(BaseTestCase):
def test_non_admin_cannot_regenerate_api_key_of_other_user(self):
query_creator = self.factory.create_user()
query = self.factory.create_query(user=query_creator)
other_user = self.factory.create_user()
orig_api_key = query.api_key

rv = self.make_request('post', "/api/queries/{}/regenerate_api_key".format(query.id), user=other_user)
self.assertEqual(rv.status_code, 403)

reloaded_query = models.Query.query.get(query.id)
self.assertEquals(orig_api_key, reloaded_query.api_key)

def test_admin_can_regenerate_api_key_of_other_user(self):
query_creator = self.factory.create_user()
query = self.factory.create_query(user=query_creator)
admin_user = self.factory.create_admin()
orig_api_key = query.api_key

rv = self.make_request('post', "/api/queries/{}/regenerate_api_key".format(query.id), user=admin_user)
self.assertEqual(rv.status_code, 200)

reloaded_query = models.Query.query.get(query.id)
self.assertNotEquals(orig_api_key, reloaded_query.api_key)

def test_admin_can_regenerate_api_key_of_myself(self):
query_creator = self.factory.create_user()
admin_user = self.factory.create_admin()
query = self.factory.create_query(user=query_creator)
orig_api_key = query.api_key

rv = self.make_request('post', "/api/queries/{}/regenerate_api_key".format(query.id), user=admin_user)
self.assertEqual(rv.status_code, 200)

updated_query = models.Query.query.get(query.id)
self.assertNotEquals(orig_api_key, updated_query.api_key)

def test_user_can_regenerate_api_key_of_myself(self):
user = self.factory.create_user()
query = self.factory.create_query(user=user)
orig_api_key = query.api_key

rv = self.make_request('post', "/api/queries/{}/regenerate_api_key".format(query.id), user=user)
self.assertEqual(rv.status_code, 200)

updated_query = models.Query.query.get(query.id)
self.assertNotEquals(orig_api_key, updated_query.api_key)


class TestQueryForkResourcePost(BaseTestCase):
def test_forks_a_query(self):
ds = self.factory.create_data_source(group=self.factory.org.default_group, view_only=False)
Expand Down

0 comments on commit 2e1c70e

Please sign in to comment.