diff --git a/ckan/logic/auth/create.py b/ckan/logic/auth/create.py index ee34afa43a0..7e16d2cc104 100644 --- a/ckan/logic/auth/create.py +++ b/ckan/logic/auth/create.py @@ -78,11 +78,11 @@ def resource_create(context, data_dict): def resource_view_create(context, data_dict): - return resource_create(context, {'id': data_dict['resource_id']}) + return authz.is_authorized('resource_create', context, {'id': data_dict['resource_id']}) def resource_create_default_resource_views(context, data_dict): - return resource_create(context, {'id': data_dict['resource']['id']}) + return authz.is_authorized('resource_create', context, {'id': data_dict['resource']['id']}) def package_create_default_resource_views(context, data_dict): @@ -208,7 +208,7 @@ def package_create_rest(context, data_dict): if not user: return {'success': False, 'msg': _('Valid API key needed to create a package')} - return package_create(context, data_dict) + return authz.is_authorized('package_create', context, data_dict) def group_create_rest(context, data_dict): model = context['model'] @@ -216,7 +216,7 @@ def group_create_rest(context, data_dict): if not user: return {'success': False, 'msg': _('Valid API key needed to create a group')} - return group_create(context, data_dict) + return authz.is_authorized('group_create', context, data_dict) def vocabulary_create(context, data_dict): # sysadmins only diff --git a/ckan/logic/auth/delete.py b/ckan/logic/auth/delete.py index ca4f2463bff..e5f50f51bfe 100644 --- a/ckan/logic/auth/delete.py +++ b/ckan/logic/auth/delete.py @@ -2,8 +2,6 @@ import ckan.authz as authz from ckan.logic.auth import get_group_object from ckan.logic.auth import get_resource_object -import ckan.logic.auth.create as _auth_create -import ckan.logic.auth.update as _auth_update from ckan.lib.base import _ @@ -15,12 +13,14 @@ def user_delete(context, data_dict): def package_delete(context, data_dict): # Defer authorization for package_delete to package_update, as deletions # are essentially changing the state field - return _auth_update.package_update(context, data_dict) + return authz.is_authorized('package_update', context, data_dict) + def dataset_purge(context, data_dict): # Only sysadmins are authorized to purge datasets return {'success': False} + def resource_delete(context, data_dict): model = context['model'] user = context.get('user') @@ -32,7 +32,7 @@ def resource_delete(context, data_dict): raise logic.NotFound(_('No package found for this resource, cannot check auth.')) pkg_dict = {'id': pkg.id} - authorized = package_delete(context, pkg_dict).get('success') + authorized = authz.is_authorized('package_delete', context, pkg_dict).get('success') if not authorized: return {'success': False, 'msg': _('User %s not authorized to delete resource %s') % (user, resource.id)} @@ -43,9 +43,9 @@ def resource_delete(context, data_dict): def resource_view_delete(context, data_dict): if context.get('resource'): - return resource_delete(context, {}) + return authz.is_authorized('resource_delete', context, {}) if context.get('resource_view'): - return resource_delete(context, {'id': context['resource_view'].resource_id}) + return authz.is_authorized('resource_delete', context, {'id': context['resource_view'].resource_id}) resource_id = data_dict.get('resource_id') if not resource_id: @@ -54,7 +54,7 @@ def resource_view_delete(context, data_dict): raise logic.NotFound(_('Resource view not found, cannot check auth.')) resource_id = resource_view.resource_id - return resource_delete(context, {'id': resource_id}) + return authz.is_authorized('resource_delete', context, {'id': resource_id}) def resource_view_clear(context, data_dict): @@ -134,4 +134,4 @@ def organization_member_delete(context, data_dict): return {'success': True} def member_delete(context, data_dict): - return _auth_create.member_create(context, data_dict) + return authz.is_authorized('member_create', context, data_dict) diff --git a/ckan/logic/auth/get.py b/ckan/logic/auth/get.py index 5d3745cc755..5d9c7a87045 100644 --- a/ckan/logic/auth/get.py +++ b/ckan/logic/auth/get.py @@ -29,31 +29,40 @@ def package_list(context, data_dict): # List of all active packages are visible by default return {'success': True} + def current_package_list_with_resources(context, data_dict): - return package_list(context, data_dict) + return authz.is_authorized('package_list', context, data_dict) + def revision_list(context, data_dict): # In our new model everyone can read the revison list return {'success': True} + def group_revision_list(context, data_dict): - return group_show(context, data_dict) + return authz.is_authorized('group_show', context, data_dict) + def organization_revision_list(context, data_dict): - return group_show(context, data_dict) + return authz.is_authorized('group_show', context, data_dict) + def package_revision_list(context, data_dict): - return package_show(context, data_dict) + return authz.is_authorized('package_show', context, data_dict) + def group_list(context, data_dict): # List of all active groups is visible by default return {'success': True} + def group_list_authz(context, data_dict): - return group_list(context, data_dict) + return authz.is_authorized('group_list', context, data_dict) + def group_list_available(context, data_dict): - return group_list(context, data_dict) + return authz.is_authorized('group_list', context, data_dict) + def organization_list(context, data_dict): # List of all active organizations are visible by default @@ -141,10 +150,12 @@ def resource_show(context, data_dict): def resource_view_show(context, data_dict): - return resource_show(context, data_dict) + return authz.is_authorized('resource_show', context, data_dict) + def resource_view_list(context, data_dict): - return resource_show(context, data_dict) + return authz.is_authorized('resource_show', context, data_dict) + def revision_show(context, data_dict): # No authz check in the logic function @@ -162,8 +173,10 @@ def group_show(context, data_dict): else: return {'success': False, 'msg': _('User %s not authorized to read group %s') % (user, group.id)} + def organization_show(context, data_dict): - return group_show(context, data_dict) + return authz.is_authorized('group_show', context, data_dict) + def vocabulary_show(context, data_dict): # Allow viewing of vocabs by default @@ -178,20 +191,26 @@ def user_show(context, data_dict): # the API key are stripped at the action level if not not logged in. return {'success': True} + def package_autocomplete(context, data_dict): - return package_list(context, data_dict) + return authz.is_authorized('package_list', context, data_dict) + def group_autocomplete(context, data_dict): - return group_list(context, data_dict) + return authz.is_authorized('group_list', context, data_dict) + def organization_autocomplete(context, data_dict): - return organization_list(context, data_dict) + return authz.is_authorized('organization_list', context, data_dict) + def tag_autocomplete(context, data_dict): - return tag_list(context, data_dict) + return authz.is_authorized('tag_list', context, data_dict) + def user_autocomplete(context, data_dict): - return user_list(context, data_dict) + return authz.is_authorized('user_list', context, data_dict) + def format_autocomplete(context, data_dict): return {'success': True} @@ -202,16 +221,19 @@ def task_status_show(context, data_dict): def resource_status_show(context, data_dict): return {'success': True} -## Modifications for rest api +## Modifications for rest api def package_show_rest(context, data_dict): - return package_show(context, data_dict) + return authz.is_authorized('package_show', context, data_dict) + def group_show_rest(context, data_dict): - return group_show(context, data_dict) + return authz.is_authorized('group_show', context, data_dict) + def tag_show_rest(context, data_dict): - return tag_show(context, data_dict) + return authz.is_authorized('tag_show', context, data_dict) + def get_site_user(context, data_dict): # FIXME this is available to sysadmins currently till @@ -243,19 +265,19 @@ def dashboard_new_activities_count(context, data_dict): def user_follower_list(context, data_dict): - return sysadmin(context, data_dict) + return authz.is_authorized('sysadmin', context, data_dict) def dataset_follower_list(context, data_dict): - return sysadmin(context, data_dict) + return authz.is_authorized('sysadmin', context, data_dict) def group_follower_list(context, data_dict): - return sysadmin(context, data_dict) + return authz.is_authorized('sysadmin', context, data_dict) def organization_follower_list(context, data_dict): - return sysadmin(context, data_dict) + return authz.is_authorized('sysadmin', context, data_dict) def _followee_list(context, data_dict): @@ -272,7 +294,7 @@ def _followee_list(context, data_dict): return {'success': True} # Sysadmins are authorized to see what anyone is following. - return sysadmin(context, data_dict) + return authz.is_authorized('sysadmin', context, data_dict) def followee_list(context, data_dict): diff --git a/ckan/logic/auth/patch.py b/ckan/logic/auth/patch.py index 75b06cd8d44..cfba5602494 100644 --- a/ckan/logic/auth/patch.py +++ b/ckan/logic/auth/patch.py @@ -1,10 +1,17 @@ -from ckan import logic -import ckan.logic.auth.update as _update +import ckan.authz as authz -package_patch = _update.package_update -resource_patch = _update.resource_update +def package_patch(context, data_dict): + return authz.is_authorized('package_update', context, data_dict) -group_patch = _update.group_update -organization_patch = _update.organization_update +def resource_patch(context, data_dict): + return authz.is_authorized('resource_update', context, data_dict) + + +def group_patch(context, data_dict): + return authz.is_authorized('group_update', context, data_dict) + + +def organization_patch(context, data_dict): + return authz.is_authorized('organization_update', context, data_dict) diff --git a/ckan/logic/auth/update.py b/ckan/logic/auth/update.py index bee4e1d7f38..96661d25c19 100644 --- a/ckan/logic/auth/update.py +++ b/ckan/logic/auth/update.py @@ -73,10 +73,10 @@ def resource_update(context, data_dict): def resource_view_update(context, data_dict): - return resource_update(context, {'id': data_dict['resource_id']}) + return authz.is_authorized('resource_update', context, {'id': data_dict['resource_id']}) def resource_view_reorder(context, data_dict): - return resource_update(context, {'id': data_dict['resource_id']}) + return authz.is_authorized('resource_update', context, {'id': data_dict['resource_id']}) def package_relationship_update(context, data_dict): return authz.is_authorized('package_relationship_create', @@ -268,7 +268,7 @@ def group_update_rest(context, data_dict): return {'success': False, 'msg': _('Valid API key needed to edit a group')} - return group_update(context, data_dict) + return authz.is_authorized('group_update', context, data_dict) def package_owner_org_update(context, data_dict): diff --git a/ckan/tests/logic/auth/test_delete.py b/ckan/tests/logic/auth/test_delete.py index c69de35dd08..45ef57afb17 100644 --- a/ckan/tests/logic/auth/test_delete.py +++ b/ckan/tests/logic/auth/test_delete.py @@ -48,7 +48,7 @@ def test_org_user_can_delete(self): user=user) response = auth_delete.resource_delete( - {'user': user['name'], 'model': model}, + {'user': user['name'], 'model': model, 'auth_user_obj': user}, {'id': dataset['resources'][0]['id']}) assert_equals(response['success'], True) diff --git a/ckanext/example_iauthfunctions/plugin_v6_parent_auth_functions.py b/ckanext/example_iauthfunctions/plugin_v6_parent_auth_functions.py new file mode 100644 index 00000000000..a034fb3069b --- /dev/null +++ b/ckanext/example_iauthfunctions/plugin_v6_parent_auth_functions.py @@ -0,0 +1,16 @@ +import pylons.config as config + +import ckan.plugins as plugins +import ckan.plugins.toolkit as toolkit + + +def package_delete(context, data_dict=None): + return {'success': False, + 'msg': 'Only sysadmins can delete packages'} + + +class ExampleIAuthFunctionsPlugin(plugins.SingletonPlugin): + plugins.implements(plugins.IAuthFunctions) + + def get_auth_functions(self): + return {'package_delete': package_delete} diff --git a/ckanext/example_iauthfunctions/tests/test_example_iauthfunctions.py b/ckanext/example_iauthfunctions/tests/test_example_iauthfunctions.py index d63ae60d474..b92dac0e350 100644 --- a/ckanext/example_iauthfunctions/tests/test_example_iauthfunctions.py +++ b/ckanext/example_iauthfunctions/tests/test_example_iauthfunctions.py @@ -6,10 +6,79 @@ import pylons.config as config import webtest +from nose.tools import assert_raises +from nose.tools import assert_equal + import ckan.model as model import ckan.tests.legacy as tests import ckan.plugins import ckan.tests.factories as factories +import ckan.logic as logic + + +class TestExampleIAuthFunctionsPluginV6ParentAuthFunctions(object): + '''Tests for the ckanext.example_iauthfunctions.plugin module. + + Specifically tests that overriding parent auth functions will cause + child auth functions to use the overridden version. + ''' + @classmethod + def setup_class(cls): + '''Nose runs this method once to setup our test class.''' + # Test code should use CKAN's plugins.load() function to load plugins + # to be tested. + ckan.plugins.load('example_iauthfunctions_v6_parent_auth_functions') + + def teardown(self): + '''Nose runs this method after each test method in our test class.''' + # Rebuild CKAN's database after each test method, so that each test + # method runs with a clean slate. + model.repo.rebuild_db() + + @classmethod + def teardown_class(cls): + '''Nose runs this method once after all the test methods in our class + have been run. + + ''' + # We have to unload the plugin we loaded, so it doesn't affect any + # tests that run after ours. + ckan.plugins.unload('example_iauthfunctions_v6_parent_auth_functions') + + def test_resource_delete_editor(self): + '''Normally organization admins can delete resources + Our plugin prevents this by blocking delete organization. + + Ensure the delete button is not displayed (as only resource delete + is checked for showing this) + + ''' + user = factories.User() + owner_org = factories.Organization( + users=[{'name': user['id'], 'capacity': 'admin'}] + ) + dataset = factories.Dataset(owner_org=owner_org['id']) + resource = factories.Resource(package_id=dataset['id']) + with assert_raises(logic.NotAuthorized) as e: + logic.check_access('resource_delete', {'user': user['name']}, {'id': resource['id']}) + + assert_equal(e.exception.message, 'User %s not authorized to delete resource %s' % (user['name'], resource['id'])) + + def test_resource_delete_sysadmin(self): + '''Normally organization admins can delete resources + Our plugin prevents this by blocking delete organization. + + Ensure the delete button is not displayed (as only resource delete + is checked for showing this) + + ''' + user = factories.Sysadmin() + owner_org = factories.Organization( + users=[{'name': user['id'], 'capacity': 'admin'}] + ) + dataset = factories.Dataset(owner_org=owner_org['id']) + resource = factories.Resource(package_id=dataset['id']) + assert_equal(logic.check_access('resource_delete', {'user': user['name']}, {'id': resource['id']}), True) class TestExampleIAuthFunctionsCustomConfigSetting(object): diff --git a/setup.py b/setup.py index 7e24a7d4a3a..00f4005b82c 100644 --- a/setup.py +++ b/setup.py @@ -108,6 +108,7 @@ 'example_iauthfunctions_v3 = ckanext.example_iauthfunctions.plugin_v3:ExampleIAuthFunctionsPlugin', 'example_iauthfunctions_v4 = ckanext.example_iauthfunctions.plugin_v4:ExampleIAuthFunctionsPlugin', 'example_iauthfunctions_v5_custom_config_setting = ckanext.example_iauthfunctions.plugin_v5_custom_config_setting:ExampleIAuthFunctionsPlugin', + 'example_iauthfunctions_v6_parent_auth_functions = ckanext.example_iauthfunctions.plugin_v6_parent_auth_functions:ExampleIAuthFunctionsPlugin', 'example_theme_v01_empty_extension = ckanext.example_theme.v01_empty_extension.plugin:ExampleThemePlugin', 'example_theme_v02_empty_template = ckanext.example_theme.v02_empty_template.plugin:ExampleThemePlugin', 'example_theme_v03_jinja = ckanext.example_theme.v03_jinja.plugin:ExampleThemePlugin',