"""
The rule registry is in charge of keeping and executing security rules.
It is the core of this app, everything else is optionnal.
This module provides a variable, ``registry``, which is just a module-level,
default RuleRegistry instance.
A rule can be a callback or a variable that will be evaluated as bool.
"""
from __future__ import unicode_literals
import logging
from django.utils.encoding import smart_text
try:
from django.utils.module_loading import autodiscover_modules
except ImportError:
autodiscover_modules = None
from .exceptions import Denied, DoesNotExist
__all__ = ('RuleRegistry', 'registry', 'require', 'run', 'autodiscover')
[docs]class RuleRegistry(dict):
"""
Dict subclass to manage rules.
logger
The standard logging logger instance to use.
"""
def __init__(self):
self.logger = logging.getLogger('rules_light')
def __setitem__(self, key, value):
"""
Adds a debug-level log on registration.
"""
super(RuleRegistry, self).__setitem__(key, value)
self.logger.debug(u'[rules_light] "%s" registered with: %s' % (
key, self.rule_text_name(value)))
[docs] def run(self, user, name, *args, **kwargs):
"""
Run a rule, return True if whatever it returns evaluates to True.
Also logs calls with the info-level.
"""
if name not in self:
self.logger.error(u'[rules_light] Rule does not exist "%s"' % name)
raise DoesNotExist(name)
rule = self[name]
if hasattr(rule, '__call__'):
result = self[name](user, name, *args, **kwargs)
else:
result = rule
text = self.as_text(user, name, *args, **kwargs)
if result:
self.logger.info(u'[rules_light] %s passed' % text)
return True
else:
self.logger.info(u'[rules_light] %s failed' % text)
return False
[docs] def require(self, user, name, *args, **kwargs):
"""
Run a rule, raise ``rules_light.Denied`` if returned False.
Log denials with warn-level.
"""
result = self.run(user, name, *args, **kwargs)
if not result:
text = self.as_text(user, name, *args, **kwargs)
self.logger.warn(u'[rules_light] Deny %s' % text)
raise Denied(text)
[docs] def as_text(self, user, name, *args, **kwargs):
""" Format a rule to be human readable for logging """
if name not in self:
raise DoesNotExist(name)
formated_args = []
for arg in args:
formated_args.append(u'"%s"' % smart_text(arg))
for key, value in kwargs.items():
formated_args.append(u'%s="%s"' % (smart_text(key),
smart_text(value)))
formated_args = u', '.join(formated_args)
if hasattr(self[name], '__call__'):
text_name = self.rule_text_name(self[name])
if formated_args:
return u'%s(%s, "%s", %s)' % (text_name, user, name,
formated_args)
else:
return u'%s(%s, "%s")' % (text_name, user, name)
else:
return u'%s is %s' % (name, self[name])
def rule_text_name(self, rule):
if hasattr(rule, 'func_name'):
return rule.func_name
elif rule is True:
return u'True'
elif rule is False:
return u'False'
elif hasattr(rule, '__name__'):
return rule.__name__
elif hasattr(rule, '__class__'):
return rule.__class__.__name__
else:
return smart_text(rule)
registry = RuleRegistry()
[docs]def run(user, name, *args, **kwargs):
""" Proxy ``rules_light.registry.run()``. """
return registry.run(user, name, *args, **kwargs)
[docs]def require(user, name, *args, **kwargs):
""" Proxy ``rules_light.registry.require()``. """
registry.require(user, name, *args, **kwargs)
def _autodiscover(registry):
"""See documentation for autodiscover (without the underscore)"""
import copy
from django.conf import settings
from django.utils.importlib import import_module
from django.utils.module_loading import module_has_submodule
for app in settings.INSTALLED_APPS:
mod = import_module(app)
# Attempt to import the app's admin module.
try:
before_import_registry = copy.copy(registry)
import_module('%s.rules_light_registry' % app)
except:
# Reset the model registry to the state before the last import as
# this import will have to reoccur on the next request and this
# could raise NotRegistered and AlreadyRegistered exceptions
# (see #8245).
registry = before_import_registry
# Decide whether to bubble up this error. If the app just
# doesn't have an admin module, we can ignore the error
# attempting to import it, otherwise we want it to bubble up.
if module_has_submodule(mod, 'rules_light_registry'):
raise
[docs]def autodiscover():
"""
Check all apps in INSTALLED_APPS for stuff related to rules_light.
For each app, autodiscover imports ``app.rules_light_registry`` if
available, resulting in execution of ``rules_light.registry[...] = ...``
statements in that module, filling registry.
Consider a standard app called 'cities_light' with such a structure::
cities_light/
__init__.py
models.py
urls.py
views.py
rules_light_registry.py
With such a rules_light_registry.py::
import rules_light
rules_light.register('cities_light.city.read', True)
rules_light.register('cities_light.city.update',
lambda user, rulename, country: user.is_staff)
When autodiscover() imports cities_light.rules_light_registry, both
`'cities_light.city.read'` and `'cities_light.city.update'` will be
registered.
"""
if autodiscover_modules:
autodiscover_modules('rules_light_registry')
else:
_autodiscover(registry)