Skip to content

Commit

Permalink
Add read-only mode support.
Browse files Browse the repository at this point in the history
  • Loading branch information
TkTech committed Dec 13, 2016
1 parent 9d970ae commit 1ae921c
Show file tree
Hide file tree
Showing 3 changed files with 213 additions and 33 deletions.
45 changes: 34 additions & 11 deletions ckan/authz.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
import sys
import re
from logging import getLogger

from pylons import config
Expand Down Expand Up @@ -87,7 +87,7 @@ def _build(self):
self._functions.update(fetched_auth_functions)

_AuthFunctions = AuthFunctions()
#remove the class
# remove the class
del AuthFunctions


Expand All @@ -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
Expand Down Expand Up @@ -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():
Expand All @@ -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:
Expand All @@ -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']),
])

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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))

Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down
44 changes: 35 additions & 9 deletions ckan/logic/__init__.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
import functools
import logging
import re
import sys

import formencode.validators

Expand All @@ -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.
Expand Down Expand Up @@ -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'):
Expand All @@ -294,6 +295,8 @@ def check_access(action, context, data_dict=None):


_actions = {}


def clear_actions_cache():
_actions.clear()

Expand Down Expand Up @@ -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__
Expand All @@ -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 = {}
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
'''
Expand All @@ -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.
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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.
Expand All @@ -600,6 +620,7 @@ class UnknownValidator(Exception):

_validators_cache = {}


def clear_validators_cache():
_validators_cache.clear()

Expand All @@ -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')
Expand All @@ -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]
Expand All @@ -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.
'''
Expand All @@ -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 = {}
Expand Down

0 comments on commit 1ae921c

Please sign in to comment.