diff --git a/ckanext/datastore/db.py b/ckanext/datastore/db.py index fb597d7053e..21970e08402 100644 --- a/ckanext/datastore/db.py +++ b/ckanext/datastore/db.py @@ -880,12 +880,12 @@ def search_data(context, data_dict): sql_string = u'''SELECT {select}, count(*) over() AS "_full_count" {rank} FROM "{resource}" {ts_query} {where} {sort} LIMIT {limit} OFFSET {offset}'''.format( - select=select_columns, - rank=rank_column, - resource=data_dict['resource_id'], - ts_query=ts_query, - where=where_clause, - sort=sort, limit=limit, offset=offset) + select=select_columns, + rank=rank_column, + resource=data_dict['resource_id'], + ts_query=ts_query, + where=where_clause, + sort=sort, limit=limit, offset=offset) results = context['connection'].execute(sql_string, [where_values]) _insert_links(data_dict, limit, offset) @@ -1080,7 +1080,7 @@ def search(context, data_dict): id = data_dict['resource_id'] result = context['connection'].execute( u"(SELECT 1 FROM pg_tables where tablename = '{0}') union" - u"(SELECT 1 FROM pg_views where viewname = '{0}')".format(id) + u"(SELECT 1 FROM pg_views where viewname = '{0}')".format(id) ).fetchone() if not result: raise ValidationError({ @@ -1114,12 +1114,12 @@ def search_sql(context, data_dict): except ProgrammingError, e: raise ValidationError({ - 'query': [str(e)], - 'info': { - 'statement': [e.statement], - 'params': [e.params], - 'orig': [str(e.orig)] - } + 'query': [str(e)], + 'info': { + 'statement': [e.statement], + 'params': [e.params], + 'orig': [str(e.orig)] + } }) except DBAPIError, e: if int(e.orig.pgcode) == _PG_ERR_CODE['query_canceled']: diff --git a/ckanext/datastore/logic/action.py b/ckanext/datastore/logic/action.py index 1b4d323e7f2..32e37454476 100644 --- a/ckanext/datastore/logic/action.py +++ b/ckanext/datastore/logic/action.py @@ -8,6 +8,8 @@ log = logging.getLogger(__name__) _get_or_bust = logic.get_or_bust +WHITELISTED_RESOURCES = ['_table_metadata'] + def datastore_create(context, data_dict): '''Adds a new table to the datastore. @@ -224,19 +226,24 @@ def datastore_search(context, data_dict): data_dict['resource_id'] = data_dict['id'] res_id = _get_or_bust(data_dict, 'resource_id') - data_dict['connection_url'] = pylons.config.get('ckan.datastore.read_url', - pylons.config['ckan.datastore.write_url']) + data_dict['connection_url'] = pylons.config.get('ckan.datastore.write_url') - resources_sql = sqlalchemy.text(u'SELECT 1 FROM "_table_metadata" WHERE name = :id') + resources_sql = sqlalchemy.text(u'''SELECT alias_of FROM "_table_metadata" + WHERE name = :id''') results = db._get_engine(None, data_dict).execute(resources_sql, id=res_id) - res_exists = results.rowcount > 0 - if not res_exists: + if not results.rowcount > 0: raise p.toolkit.ObjectNotFound(p.toolkit._( 'Resource "{0}" was not found.'.format(res_id) )) - p.toolkit.check_access('datastore_search', context, data_dict) + if not data_dict['resource_id'] in WHITELISTED_RESOURCES: + # replace potential alias with real id to simplify access checks + resource_id = results.fetchone()[0] + if resource_id: + data_dict['resource_id'] = resource_id + + p.toolkit.check_access('datastore_search', context, data_dict) result = db.search(context, data_dict) result.pop('id', None) diff --git a/ckanext/datastore/tests/test_search.py b/ckanext/datastore/tests/test_search.py index 10b4e66c7de..d1a6a403c15 100644 --- a/ckanext/datastore/tests/test_search.py +++ b/ckanext/datastore/tests/test_search.py @@ -25,9 +25,10 @@ def setup_class(cls): ctd.CreateTestData.create() cls.sysadmin_user = model.User.get('testsysadmin') cls.normal_user = model.User.get('annafan') - resource = model.Package.get('annakarenina').resources[0] + cls.dataset = model.Package.get('annakarenina') + cls.resource = cls.dataset.resources[0] cls.data = { - 'resource_id': resource.id, + 'resource_id': cls.resource.id, 'aliases': 'books3', 'fields': [{'id': u'b\xfck', 'type': 'text'}, {'id': 'author', 'type': 'text'}, @@ -75,7 +76,7 @@ def teardown_class(cls): def test_search_basic(self): data = {'resource_id': self.data['resource_id']} postparams = '%s=1' % json.dumps(data) - auth = {'Authorization': str(self.sysadmin_user.apikey)} + auth = {'Authorization': str(self.normal_user.apikey)} res = self.app.post('/api/action/datastore_search', params=postparams, extra_environ=auth) res_dict = json.loads(res.body) @@ -87,7 +88,7 @@ def test_search_basic(self): # search with parameter id should yield the same results data = {'id': self.data['resource_id']} postparams = '%s=1' % json.dumps(data) - auth = {'Authorization': str(self.sysadmin_user.apikey)} + auth = {'Authorization': str(self.normal_user.apikey)} res = self.app.post('/api/action/datastore_search', params=postparams, extra_environ=auth) res_dict = json.loads(res.body) @@ -111,7 +112,7 @@ def test_search_invalid_field(self): data = {'resource_id': self.data['resource_id'], 'fields': [{'id': 'bad'}]} postparams = '%s=1' % json.dumps(data) - auth = {'Authorization': str(self.sysadmin_user.apikey)} + auth = {'Authorization': str(self.normal_user.apikey)} res = self.app.post('/api/action/datastore_search', params=postparams, extra_environ=auth, status=409) res_dict = json.loads(res.body) @@ -121,7 +122,7 @@ def test_search_fields(self): data = {'resource_id': self.data['resource_id'], 'fields': [u'b\xfck']} postparams = '%s=1' % json.dumps(data) - auth = {'Authorization': str(self.sysadmin_user.apikey)} + auth = {'Authorization': str(self.normal_user.apikey)} res = self.app.post('/api/action/datastore_search', params=postparams, extra_environ=auth) res_dict = json.loads(res.body) @@ -134,7 +135,7 @@ def test_search_fields(self): data = {'resource_id': self.data['resource_id'], 'fields': u'b\xfck, author'} postparams = '%s=1' % json.dumps(data) - auth = {'Authorization': str(self.sysadmin_user.apikey)} + auth = {'Authorization': str(self.normal_user.apikey)} res = self.app.post('/api/action/datastore_search', params=postparams, extra_environ=auth) res_dict = json.loads(res.body) @@ -148,7 +149,7 @@ def test_search_filters(self): data = {'resource_id': self.data['resource_id'], 'filters': {u'b\xfck': 'annakarenina'}} postparams = '%s=1' % json.dumps(data) - auth = {'Authorization': str(self.sysadmin_user.apikey)} + auth = {'Authorization': str(self.normal_user.apikey)} res = self.app.post('/api/action/datastore_search', params=postparams, extra_environ=auth) res_dict = json.loads(res.body) @@ -161,7 +162,7 @@ def test_search_array_filters(self): data = {'resource_id': self.data['resource_id'], 'filters': {u'characters': [u'Princess Anna', u'Sergius']}} postparams = '%s=1' % json.dumps(data) - auth = {'Authorization': str(self.sysadmin_user.apikey)} + auth = {'Authorization': str(self.normal_user.apikey)} res = self.app.post('/api/action/datastore_search', params=postparams, extra_environ=auth) res_dict = json.loads(res.body) @@ -174,7 +175,7 @@ def test_search_sort(self): data = {'resource_id': self.data['resource_id'], 'sort': u'b\xfck asc, author desc'} postparams = '%s=1' % json.dumps(data) - auth = {'Authorization': str(self.sysadmin_user.apikey)} + auth = {'Authorization': str(self.normal_user.apikey)} res = self.app.post('/api/action/datastore_search', params=postparams, extra_environ=auth) res_dict = json.loads(res.body) @@ -200,7 +201,7 @@ def test_search_limit(self): data = {'resource_id': self.data['resource_id'], 'limit': 1} postparams = '%s=1' % json.dumps(data) - auth = {'Authorization': str(self.sysadmin_user.apikey)} + auth = {'Authorization': str(self.normal_user.apikey)} res = self.app.post('/api/action/datastore_search', params=postparams, extra_environ=auth) res_dict = json.loads(res.body) @@ -213,7 +214,7 @@ def test_search_invalid_limit(self): data = {'resource_id': self.data['resource_id'], 'limit': 'bad'} postparams = '%s=1' % json.dumps(data) - auth = {'Authorization': str(self.sysadmin_user.apikey)} + auth = {'Authorization': str(self.normal_user.apikey)} res = self.app.post('/api/action/datastore_search', params=postparams, extra_environ=auth, status=409) res_dict = json.loads(res.body) @@ -222,7 +223,7 @@ def test_search_invalid_limit(self): data = {'resource_id': self.data['resource_id'], 'limit': -1} postparams = '%s=1' % json.dumps(data) - auth = {'Authorization': str(self.sysadmin_user.apikey)} + auth = {'Authorization': str(self.normal_user.apikey)} res = self.app.post('/api/action/datastore_search', params=postparams, extra_environ=auth, status=409) res_dict = json.loads(res.body) @@ -233,7 +234,7 @@ def test_search_offset(self): 'limit': 1, 'offset': 1} postparams = '%s=1' % json.dumps(data) - auth = {'Authorization': str(self.sysadmin_user.apikey)} + auth = {'Authorization': str(self.normal_user.apikey)} res = self.app.post('/api/action/datastore_search', params=postparams, extra_environ=auth) res_dict = json.loads(res.body) @@ -246,7 +247,7 @@ def test_search_invalid_offset(self): data = {'resource_id': self.data['resource_id'], 'offset': 'bad'} postparams = '%s=1' % json.dumps(data) - auth = {'Authorization': str(self.sysadmin_user.apikey)} + auth = {'Authorization': str(self.normal_user.apikey)} res = self.app.post('/api/action/datastore_search', params=postparams, extra_environ=auth, status=409) res_dict = json.loads(res.body) @@ -255,7 +256,7 @@ def test_search_invalid_offset(self): data = {'resource_id': self.data['resource_id'], 'offset': -1} postparams = '%s=1' % json.dumps(data) - auth = {'Authorization': str(self.sysadmin_user.apikey)} + auth = {'Authorization': str(self.normal_user.apikey)} res = self.app.post('/api/action/datastore_search', params=postparams, extra_environ=auth, status=409) res_dict = json.loads(res.body) @@ -266,7 +267,7 @@ def test_search_full_text(self): 'q': 'annakarenina'} postparams = '%s=1' % json.dumps(data) - auth = {'Authorization': str(self.sysadmin_user.apikey)} + auth = {'Authorization': str(self.normal_user.apikey)} res = self.app.post('/api/action/datastore_search', params=postparams, extra_environ=auth) res_dict = json.loads(res.body) @@ -325,7 +326,7 @@ def test_search_full_text(self): def test_search_table_metadata(self): data = {'resource_id': "_table_metadata"} postparams = '%s=1' % json.dumps(data) - auth = {'Authorization': str(self.sysadmin_user.apikey)} + auth = {'Authorization': str(self.normal_user.apikey)} res = self.app.post('/api/action/datastore_search', params=postparams, extra_environ=auth) res_dict = json.loads(res.body) @@ -365,7 +366,7 @@ def setup_class(cls): ] ) postparams = '%s=1' % json.dumps(cls.data) - auth = {'Authorization': str(cls.sysadmin_user.apikey)} + auth = {'Authorization': str(cls.normal_user.apikey)} res = cls.app.post('/api/action/datastore_create', params=postparams, extra_environ=auth) res_dict = json.loads(res.body) @@ -380,7 +381,7 @@ def test_search_full_text(self): data = {'resource_id': self.data['resource_id'], 'q': 'DE'} postparams = '%s=1' % json.dumps(data) - auth = {'Authorization': str(self.sysadmin_user.apikey)} + auth = {'Authorization': str(self.normal_user.apikey)} res = self.app.post('/api/action/datastore_search', params=postparams, extra_environ=auth) res_dict = json.loads(res.body) @@ -391,7 +392,7 @@ def test_advanced_search_full_text(self): 'plain': 'False', 'q': 'DE | UK'} postparams = '%s=1' % json.dumps(data) - auth = {'Authorization': str(self.sysadmin_user.apikey)} + auth = {'Authorization': str(self.normal_user.apikey)} res = self.app.post('/api/action/datastore_search', params=postparams, extra_environ=auth) res_dict = json.loads(res.body) @@ -474,7 +475,7 @@ def test_invalid_statement(self): query = 'SELECT ** FROM public.foobar' data = {'sql': query} postparams = json.dumps(data) - auth = {'Authorization': str(self.sysadmin_user.apikey)} + auth = {'Authorization': str(self.normal_user.apikey)} res = self.app.post('/api/action/datastore_search_sql', params=postparams, extra_environ=auth, status=409) res_dict = json.loads(res.body) @@ -484,7 +485,7 @@ def test_select_basic(self): query = 'SELECT * FROM public."{0}"'.format(self.data['resource_id']) data = {'sql': query} postparams = json.dumps(data) - auth = {'Authorization': str(self.sysadmin_user.apikey)} + auth = {'Authorization': str(self.normal_user.apikey)} res = self.app.post('/api/action/datastore_search_sql', params=postparams, extra_environ=auth) res_dict = json.loads(res.body) @@ -512,7 +513,7 @@ def test_self_join(self): '''.format(self.data['resource_id']) data = {'sql': query} postparams = json.dumps(data) - auth = {'Authorization': str(self.sysadmin_user.apikey)} + auth = {'Authorization': str(self.normal_user.apikey)} res = self.app.post('/api/action/datastore_search_sql', params=postparams, extra_environ=auth) res_dict = json.loads(res.body)