diff --git a/ckanext/datastore/db.py b/ckanext/datastore/db.py index a9cc3795241..8312c80541f 100644 --- a/ckanext/datastore/db.py +++ b/ckanext/datastore/db.py @@ -13,6 +13,7 @@ import sqlalchemy from sqlalchemy.exc import ProgrammingError, IntegrityError, DBAPIError import psycopg2.extras +import ckan.lib.cli as cli log = logging.getLogger(__name__) @@ -1129,3 +1130,39 @@ def search_sql(context, data_dict): raise finally: context['connection'].close() + + +def _get_read_only_user(data_dict): + parsed = cli.parse_db_config('ckan.datastore.read_url') + return parsed['db_user'] + + +def _change_privilege(context, data_dict, what): + engine = _get_engine(context, data_dict) + context['connection'] = engine.connect() + read_only_user = _get_read_only_user(data_dict) + assert(what in ['REVOKE', 'GRANT']) + if what == 'REVOKE': + sql = u'REVOKE SELECT ON TABLE "{0}" FROM "{1}"'.format( + data_dict['resource_id'], + read_only_user) + elif what == 'GRANT': + sql = u'GRANT SELECT ON TABLE "{0}" TO "{1}"'.format( + data_dict['resource_id'], + read_only_user) + try: + context['connection'].execute(sql) + except ProgrammingError, e: + log.critical("Error making resource private. {0}".format(e.message)) + raise ValidationError({ + 'privileges': [u'cannot make "{0}" private'.format( + data_dict['resource_id'])], + }) + + +def make_private(context, data_dict): + _change_privilege(context, data_dict, 'REVOKE') + + +def make_public(context, data_dict): + _change_privilege(context, data_dict, 'GRANT') diff --git a/ckanext/datastore/logic/action.py b/ckanext/datastore/logic/action.py index 859172d1999..9769975492c 100644 --- a/ckanext/datastore/logic/action.py +++ b/ckanext/datastore/logic/action.py @@ -287,3 +287,37 @@ def datastore_search_sql(context, data_dict): result.pop('id', None) result.pop('connection_url') return result + + +def datastore_make_private(context, data_dict): + model = _get_or_bust(context, 'model') + if 'id' in data_dict: + data_dict['resource_id'] = data_dict['id'] + res_id = _get_or_bust(data_dict, 'resource_id') + + if not model.Resource.get(res_id): + raise p.toolkit.ObjectNotFound(p.toolkit._( + 'Resource "{0}" was not found.'.format(res_id) + )) + + p.toolkit.check_access('datastore_change_permissions', context, data_dict) + + data_dict['connection_url'] = pylons.config.get('ckan.datastore.write_url') + db.make_private(context, data_dict) + + +def datastore_make_public(context, data_dict): + model = _get_or_bust(context, 'model') + if 'id' in data_dict: + data_dict['resource_id'] = data_dict['id'] + res_id = _get_or_bust(data_dict, 'resource_id') + + if not model.Resource.get(res_id): + raise p.toolkit.ObjectNotFound(p.toolkit._( + 'Resource "{0}" was not found.'.format(res_id) + )) + + p.toolkit.check_access('datastore_change_permissions', context, data_dict) + + data_dict['connection_url'] = pylons.config.get('ckan.datastore.write_url') + db.make_public(context, data_dict) diff --git a/ckanext/datastore/logic/auth.py b/ckanext/datastore/logic/auth.py index 867e1358b16..641eb5cdddf 100644 --- a/ckanext/datastore/logic/auth.py +++ b/ckanext/datastore/logic/auth.py @@ -32,3 +32,7 @@ def datastore_delete(context, data_dict): def datastore_search(context, data_dict): return _datastore_auth(context, data_dict, 'resource_show') + + +def datastore_change_permissions(context, data_dict): + return _datastore_auth(context, data_dict) diff --git a/ckanext/datastore/plugin.py b/ckanext/datastore/plugin.py index 50b792f122e..bbce78149ec 100644 --- a/ckanext/datastore/plugin.py +++ b/ckanext/datastore/plugin.py @@ -201,7 +201,9 @@ def get_actions(self): actions = {'datastore_create': action.datastore_create, 'datastore_upsert': action.datastore_upsert, 'datastore_delete': action.datastore_delete, - 'datastore_search': action.datastore_search} + 'datastore_search': action.datastore_search, + 'datastore_make_private': action.datastore_make_private, + 'datastore_make_public': action.datastore_make_public} if not self.legacy_mode: actions['datastore_search_sql'] = action.datastore_search_sql return actions @@ -210,4 +212,5 @@ def get_auth_functions(self): return {'datastore_create': auth.datastore_create, 'datastore_upsert': auth.datastore_upsert, 'datastore_delete': auth.datastore_delete, - 'datastore_search': auth.datastore_search} + 'datastore_search': auth.datastore_search, + 'datastore_change_permissions': auth.datastore_change_permissions} diff --git a/ckanext/datastore/tests/test_search.py b/ckanext/datastore/tests/test_search.py index 99794e3f84b..30e0f845b38 100644 --- a/ckanext/datastore/tests/test_search.py +++ b/ckanext/datastore/tests/test_search.py @@ -561,3 +561,30 @@ def test_self_join(self): assert res_dict['success'] is True result = res_dict['result'] assert result['records'] == self.expected_join_results + + def test_read_private(self): + from pylons import config + context = { + 'user': self.sysadmin_user.name, + 'model': model} + data_dict = { + 'resource_id': self.data['resource_id'], + 'connection_url': config['ckan.datastore.write_url']} + p.toolkit.get_action('datastore_make_private')(context, data_dict) + ''' + data = {'resource_id': self.data['resource_id']} + postparams = json.dumps(data) + auth = {'Authorization': str(self.sysadmin_user.apikey)} + res = self.app.post('/api/action/datastore_make_private', params=postparams, + extra_environ=auth) + res_dict = json.loads(res.body) + assert res_dict['success'] is True + ''' + query = 'SELECT * FROM "{0}"'.format(self.data['resource_id']) + data = {'sql': query} + postparams = json.dumps(data) + 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) + assert res_dict['success'] is False