diff --git a/ckan/authz.py b/ckan/authz.py index c95c85f5f1f..03a7fc58a49 100644 --- a/ckan/authz.py +++ b/ckan/authz.py @@ -1,5 +1,5 @@ +# -*- coding: utf-8 -*- import sys -import re from logging import getLogger from pylons import config @@ -87,7 +87,7 @@ def _build(self): self._functions.update(fetched_auth_functions) _AuthFunctions = AuthFunctions() -#remove the class +# remove the class del AuthFunctions @@ -109,7 +109,8 @@ def is_sysadmin(username): def _get_user(username): - ''' Try to get the user from c, if possible, and fallback to using the DB ''' + '''Try to get the user from c, if possible, and fallback to using the DB + ''' if not username: return None # See if we can get the user without touching the DB @@ -146,11 +147,22 @@ def is_authorized(action, context, data_dict=None): if context.get('ignore_auth'): return {'success': True} + read_only = asbool(config.get('ckan.read_only', 'false')) + auth_function = _AuthFunctions.get(action) if auth_function: username = context.get('user') - user = _get_user(username) + if read_only: + # If the auth function has not been wrapped with a auth_read_safe + # decorator, we should deny it if read-only mode is on. + if not getattr(auth_function, 'auth_read_safe', False): + return { + 'success': False, + 'msg': 'Read-only mode is enabled.' + } + + user = _get_user(username) if user: # deleted users are always unauthorized if user.is_deleted(): @@ -167,10 +179,12 @@ def is_authorized(action, context, data_dict=None): # access straight away if not getattr(auth_function, 'auth_allow_anonymous_access', False) \ and not context.get('auth_user_obj'): - return {'success': False, - 'msg': '{0} requires an authenticated user' - .format(auth_function) - } + return { + 'success': False, + 'msg': '{0} requires an authenticated user'.format( + auth_function + ) + } return auth_function(context, data_dict) else: @@ -180,7 +194,13 @@ def is_authorized(action, context, data_dict=None): # these are the permissions that roles have ROLE_PERMISSIONS = OrderedDict([ ('admin', ['admin']), - ('editor', ['read', 'delete_dataset', 'create_dataset', 'update_dataset', 'manage_group']), + ('editor', [ + 'read', + 'delete_dataset', + 'create_dataset', + 'update_dataset', + 'manage_group' + ]), ('member', ['read', 'manage_group']), ]) @@ -251,7 +271,8 @@ def has_user_permission_for_group_or_org(group_id, user_name, permission): return True # Handle when permissions cascade. Check the user's roles on groups higher # in the group hierarchy for permission. - for capacity in check_config_permission('roles_that_cascade_to_sub_groups'): + for capacity in check_config_permission( + 'roles_that_cascade_to_sub_groups'): parent_groups = group.get_parent_group_hierarchy(type=group.type) group_ids = [group_.id for group_ in parent_groups] if _has_user_permission_for_groups(user_id, permission, group_ids, @@ -333,7 +354,7 @@ def has_user_permission_for_some_org(user_name, permission): # see if any of the groups are orgs q = model.Session.query(model.Group) \ - .filter(model.Group.is_organization == True) \ + .filter(model.Group.is_organization.is_(True)) \ .filter(model.Group.state == 'active') \ .filter(model.Group.id.in_(group_ids)) @@ -415,6 +436,7 @@ def auth_is_registered_user(): ''' return auth_is_loggedin_user() + def auth_is_loggedin_user(): ''' Do we have a logged in user ''' try: @@ -423,6 +445,7 @@ def auth_is_loggedin_user(): context_user = None return bool(context_user) + def auth_is_anon_user(context): ''' Is this an anonymous user? eg Not logged in if a web request and not user defined in context diff --git a/ckan/logic/__init__.py b/ckan/logic/__init__.py index 2bc7626af4f..767b210170a 100644 --- a/ckan/logic/__init__.py +++ b/ckan/logic/__init__.py @@ -1,7 +1,7 @@ +# -*- coding: utf-8 -*- import functools import logging import re -import sys import formencode.validators @@ -27,6 +27,7 @@ class UsernamePasswordError(Exception): class ActionError(Exception): pass + class NotFound(ActionError): '''Exception raised by logic functions when a given object is not found. @@ -267,7 +268,7 @@ def check_access(action, context, data_dict=None): user = context.get('user') try: - if not 'auth_user_obj' in context: + if 'auth_user_obj' not in context: context['auth_user_obj'] = None if not context.get('ignore_auth'): @@ -294,6 +295,8 @@ def check_access(action, context, data_dict=None): _actions = {} + + def clear_actions_cache(): _actions.clear() @@ -343,9 +346,10 @@ def get_action(action): ''' if _actions: - if not action in _actions: + if action not in _actions: raise KeyError("Action '%s' not found" % action) return _actions.get(action) + # Otherwise look in all the plugins to resolve all possible # First get the default ones in the ckan/logic/action directory # Rather than writing them out in full will use __import__ @@ -371,7 +375,6 @@ def get_action(action): not hasattr(v, 'side_effect_free'): v.side_effect_free = True - # Then overwrite them with any specific ones in the plugins: resolved_action_plugins = {} fetched_actions = {} @@ -421,8 +424,11 @@ def wrapped(context=None, data_dict=None, **kw): log.debug('No auth function for %s' % action_name) elif not getattr(_action, 'auth_audit_exempt', False): raise Exception( - 'Action function {0} did not call its auth function' - .format(action_name)) + 'Action function {0} did not call its auth' + ' function'.format( + action_name + ) + ) # remove from audit stack context['__auth_audit'].pop() except IndexError: @@ -485,6 +491,7 @@ def get_or_bust(data_dict, keys): return values[0] return tuple(values) + def validate(schema_func, can_skip_validator=False): ''' A decorator that validates an action function against a given schema ''' @@ -503,6 +510,7 @@ def wrapper(context, data_dict): return wrapper return action_decorator + def side_effect_free(action): '''A decorator that marks the given action function as side-effect-free. @@ -556,6 +564,15 @@ def wrapper(context, data_dict): return wrapper +def auth_read_safe(action): + @functools.wraps(action) + def wrapper(context, data_dict): + return action(context, data_dict) + + wrapper.auth_read_safe = True + return wrapper + + def auth_audit_exempt(action): ''' Dirty hack to stop auth audit being done ''' @functools.wraps(action) @@ -564,6 +581,7 @@ def wrapper(context, data_dict): wrapper.auth_audit_exempt = True return wrapper + def auth_allow_anonymous_access(action): ''' Flag an auth function as not requiring a logged in user @@ -578,6 +596,7 @@ def wrapper(context, data_dict): wrapper.auth_allow_anonymous_access = True return wrapper + def auth_disallow_anonymous_access(action): ''' Flag an auth function as requiring a logged in user @@ -591,6 +610,7 @@ def wrapper(context, data_dict): wrapper.auth_allow_anonymous_access = False return wrapper + class UnknownValidator(Exception): '''Exception raised when a requested validator function cannot be found. @@ -600,6 +620,7 @@ class UnknownValidator(Exception): _validators_cache = {} + def clear_validators_cache(): _validators_cache.clear() @@ -620,7 +641,7 @@ def get_validator(validator): :rtype: ``types.FunctionType`` ''' - if not _validators_cache: + if not _validators_cache: validators = _import_module_functions('ckan.lib.navl.validators') _validators_cache.update(validators) validators = _import_module_functions('ckan.logic.validators') @@ -635,7 +656,10 @@ def get_validator(validator): raise NameConflict( 'The validator %r is already defined' % (name,) ) - log.debug('Validator function {0} from plugin {1} was inserted'.format(name, plugin.name)) + log.debug( + 'Validator function {0} from plugin {1}' + ' was inserted'.format(name, plugin.name) + ) _validators_cache[name] = fn try: return _validators_cache[validator] @@ -644,7 +668,8 @@ def get_validator(validator): def model_name_to_class(model_module, model_name): - '''Return the class in model_module that has the same name as the received string. + '''Return the class in model_module that has the same name as the received + string. Raises AttributeError if there's no model in model_module named model_name. ''' @@ -654,6 +679,7 @@ def model_name_to_class(model_module, model_name): except AttributeError: raise ValidationError("%s isn't a valid model" % model_class_name) + def _import_module_functions(module_path): '''Import a module and get the functions and return them in a dict''' functions_dict = {} diff --git a/ckan/logic/auth/get.py b/ckan/logic/auth/get.py index ef3748fc046..ef63667f3d8 100644 --- a/ckan/logic/auth/get.py +++ b/ckan/logic/auth/get.py @@ -1,8 +1,11 @@ import ckan.logic as logic import ckan.authz as authz from ckan.lib.base import _ -from ckan.logic.auth import (get_package_object, get_group_object, - get_resource_object, get_related_object) +from ckan.logic.auth import ( + get_package_object, + get_group_object, + get_resource_object +) def sysadmin(context, data_dict): @@ -10,6 +13,7 @@ def sysadmin(context, data_dict): return {'success': False, 'msg': _('Not authorized')} +@logic.auth_read_safe def site_read(context, data_dict): """\ This function should be deprecated. It is only here because we couldn't @@ -21,63 +25,97 @@ def site_read(context, data_dict): # FIXME we need to remove this for now we allow site read return {'success': True} + +@logic.auth_read_safe def package_search(context, data_dict): # Everyone can search by default return {'success': True} + +@logic.auth_read_safe def package_list(context, data_dict): # List of all active packages are visible by default return {'success': True} + +@logic.auth_read_safe def current_package_list_with_resources(context, data_dict): return package_list(context, data_dict) + +@logic.auth_read_safe def revision_list(context, data_dict): # In our new model everyone can read the revison list return {'success': True} + +@logic.auth_read_safe def group_revision_list(context, data_dict): return group_show(context, data_dict) + +@logic.auth_read_safe def organization_revision_list(context, data_dict): return group_show(context, data_dict) + +@logic.auth_read_safe def package_revision_list(context, data_dict): return package_show(context, data_dict) + +@logic.auth_read_safe def group_list(context, data_dict): # List of all active groups is visible by default return {'success': True} + +@logic.auth_read_safe def group_list_authz(context, data_dict): return group_list(context, data_dict) + +@logic.auth_read_safe def group_list_available(context, data_dict): return group_list(context, data_dict) + +@logic.auth_read_safe def organization_list(context, data_dict): # List of all active organizations are visible by default return {'success': True} + +@logic.auth_read_safe def organization_list_for_user(context, data_dict): return {'success': True} + +@logic.auth_read_safe def license_list(context, data_dict): # Licenses list is visible by default return {'success': True} + +@logic.auth_read_safe def vocabulary_list(context, data_dict): # List of all vocabularies are visible by default return {'success': True} + +@logic.auth_read_safe def tag_list(context, data_dict): # Tags list is visible by default return {'success': True} + +@logic.auth_read_safe def user_list(context, data_dict): # Users list is visible by default return {'success': True} + +@logic.auth_read_safe def package_relationships_list(context, data_dict): user = context.get('user') @@ -94,18 +132,26 @@ def package_relationships_list(context, data_dict): authorized2 = True if not (authorized1 and authorized2): - return {'success': False, 'msg': _('User %s not authorized to read these packages') % user} + return { + 'success': False, + 'msg': _('User %s not authorized to read these packages') % user + } else: return {'success': True} + +@logic.auth_read_safe def package_show(context, data_dict): user = context.get('user') package = get_package_object(context, data_dict) # draft state indicates package is still in the creation process # so we need to check we have creation rights. if package.state.startswith('draft'): - auth = authz.is_authorized('package_update', - context, data_dict) + auth = authz.is_authorized( + 'package_update', + context, + data_dict + ) authorized = auth.get('success') elif package.owner_org is None and package.state == 'active': return {'success': True} @@ -116,14 +162,23 @@ def package_show(context, data_dict): authorized = authz.has_user_permission_for_group_or_org( package.owner_org, user, 'read') if not authorized: - return {'success': False, 'msg': _('User %s not authorized to read package %s') % (user, package.id)} + return { + 'success': False, + 'msg': _('User %s not authorized to read package %s') % ( + user, + package.id + ) + } else: return {'success': True} + +@logic.auth_read_safe def related_show(context, data_dict=None): return {'success': True} +@logic.auth_read_safe def resource_show(context, data_dict): model = context['model'] user = context.get('user') @@ -132,27 +187,46 @@ def resource_show(context, data_dict): # check authentication against package pkg = model.Package.get(resource.package_id) if not pkg: - raise logic.NotFound(_('No package found for this resource, cannot check auth.')) + raise logic.NotFound( + _('No package found for this resource, cannot check auth.') + ) pkg_dict = {'id': pkg.id} - authorized = authz.is_authorized('package_show', context, pkg_dict).get('success') + authorized = authz.is_authorized( + 'package_show', + context, + pkg_dict + ).get('success') if not authorized: - return {'success': False, 'msg': _('User %s not authorized to read resource %s') % (user, resource.id)} + return { + 'success': False, + 'msg': _('User %s not authorized to read resource %s') % ( + user, + resource.id + ) + } else: return {'success': True} +@logic.auth_read_safe def resource_view_show(context, data_dict): return resource_show(context, data_dict) + +@logic.auth_read_safe def resource_view_list(context, data_dict): return resource_show(context, data_dict) + +@logic.auth_read_safe def revision_show(context, data_dict): # No authz check in the logic function return {'success': True} + +@logic.auth_read_safe def group_show(context, data_dict): user = context.get('user') group = get_group_object(context, data_dict) @@ -163,59 +237,95 @@ def group_show(context, data_dict): if authorized: return {'success': True} else: - return {'success': False, 'msg': _('User %s not authorized to read group %s') % (user, group.id)} + return { + 'success': False, + 'msg': _('User %s not authorized to read group %s') % ( + user, + group.id + ) + } + +@logic.auth_read_safe def organization_show(context, data_dict): return group_show(context, data_dict) + +@logic.auth_read_safe def vocabulary_show(context, data_dict): # Allow viewing of vocabs by default return {'success': True} + +@logic.auth_read_safe def tag_show(context, data_dict): # No authz check in the logic function return {'success': True} + +@logic.auth_read_safe def user_show(context, data_dict): # By default, user details can be read by anyone, but some properties like # the API key are stripped at the action level if not not logged in. return {'success': True} + +@logic.auth_read_safe def package_autocomplete(context, data_dict): return package_list(context, data_dict) + +@logic.auth_read_safe def group_autocomplete(context, data_dict): return group_list(context, data_dict) + +@logic.auth_read_safe def organization_autocomplete(context, data_dict): return organization_list(context, data_dict) + +@logic.auth_read_safe def tag_autocomplete(context, data_dict): return tag_list(context, data_dict) + +@logic.auth_read_safe def user_autocomplete(context, data_dict): return user_list(context, data_dict) + +@logic.auth_read_safe def format_autocomplete(context, data_dict): return {'success': True} + +@logic.auth_read_safe def task_status_show(context, data_dict): return {'success': True} + +@logic.auth_read_safe def resource_status_show(context, data_dict): return {'success': True} -## Modifications for rest api +@logic.auth_read_safe def package_show_rest(context, data_dict): return package_show(context, data_dict) + +@logic.auth_read_safe def group_show_rest(context, data_dict): return group_show(context, data_dict) + +@logic.auth_read_safe def tag_show_rest(context, data_dict): return tag_show(context, data_dict) + +@logic.auth_read_safe def get_site_user(context, data_dict): # FIXME this is available to sysadmins currently till # @auth_sysadmins_check decorator is added @@ -223,10 +333,12 @@ def get_site_user(context, data_dict): 'msg': 'Only internal services allowed to use this action'} +@logic.auth_read_safe def member_roles_list(context, data_dict): return {'success': True} +@logic.auth_read_safe def dashboard_activity_list(context, data_dict): # FIXME: context['user'] could be an IP address but that case is not # handled here. Maybe add an auth helper function like is_logged_in(). @@ -237,30 +349,39 @@ def dashboard_activity_list(context, data_dict): 'msg': _("You must be logged in to access your dashboard.")} +@logic.auth_read_safe def dashboard_new_activities_count(context, data_dict): # FIXME: This should go through check_access() not call is_authorized() # directly, but wait until 2939-orgs is merged before fixing this. # This is so a better not authourized message can be sent. - return authz.is_authorized('dashboard_activity_list', - context, data_dict) + return authz.is_authorized( + 'dashboard_activity_list', + context, + data_dict + ) +@logic.auth_read_safe def user_follower_list(context, data_dict): return sysadmin(context, data_dict) +@logic.auth_read_safe def dataset_follower_list(context, data_dict): return sysadmin(context, data_dict) +@logic.auth_read_safe def group_follower_list(context, data_dict): return sysadmin(context, data_dict) +@logic.auth_read_safe def organization_follower_list(context, data_dict): return sysadmin(context, data_dict) +@logic.auth_read_safe def _followee_list(context, data_dict): model = context['model'] @@ -278,47 +399,57 @@ def _followee_list(context, data_dict): return sysadmin(context, data_dict) +@logic.auth_read_safe def followee_list(context, data_dict): return _followee_list(context, data_dict) +@logic.auth_read_safe @logic.auth_audit_exempt def user_followee_list(context, data_dict): return _followee_list(context, data_dict) +@logic.auth_read_safe @logic.auth_audit_exempt def dataset_followee_list(context, data_dict): return _followee_list(context, data_dict) +@logic.auth_read_safe @logic.auth_audit_exempt def group_followee_list(context, data_dict): return _followee_list(context, data_dict) +@logic.auth_read_safe @logic.auth_audit_exempt def organization_followee_list(context, data_dict): return _followee_list(context, data_dict) +@logic.auth_read_safe def user_reset(context, data_dict): return {'success': True} +@logic.auth_read_safe def request_reset(context, data_dict): return {'success': True} +@logic.auth_read_safe def help_show(context, data_dict): return {'success': True} +@logic.auth_read_safe def config_option_show(context, data_dict): '''Show runtime-editable configuration option. Only sysadmins.''' return {'success': False} +@logic.auth_read_safe def config_option_list(context, data_dict): '''List runtime-editable configuration options. Only sysadmins.''' return {'success': False}