diff --git a/TODO.md b/TODO.md
index f006cdf7a7..c8673cb930 100644
--- a/TODO.md
+++ b/TODO.md
@@ -5,9 +5,6 @@ CC TODO
=======
* supervisord to start celery, modify ansible playbook to set up supervisord <- ChrisC
* documentation on how to run with callbacks from NOT a launchjob <-- ChrisC
-* interactive SSH agent support for launch jobs/creds <-- ChrisC
-* michael to modify ansible to accept ssh password and sudo password from env vars
-* way to send cntrl-c to kill job (method on job?) <-- ChrisC, low priority
* default_playbook should be relative to scm_repository and not allow "../" out of the directory
* do we need something other than default playbook (ProjectOptions) <-- MPD later
diff --git a/lib/main/base_views.py b/lib/main/base_views.py
index e8480fdc8b..3c41b0c849 100644
--- a/lib/main/base_views.py
+++ b/lib/main/base_views.py
@@ -20,7 +20,7 @@ from lib.main.models import *
from django.contrib.auth.models import User
from lib.main.serializers import *
from lib.main.rbac import *
-from django.core.exceptions import PermissionDenied
+from rest_framework.exceptions import PermissionDenied
from rest_framework import mixins
from rest_framework import generics
from rest_framework import permissions
@@ -36,20 +36,21 @@ class BaseList(generics.ListCreateAPIView):
def list_permissions_check(self, request, obj=None):
''' determines some early yes/no access decisions, pre-filtering '''
- if request.method == 'GET':
- return True
+ #print '---', request.method, getattr(request, '_method', None)
+ if request.method in ('OPTIONS', 'HEAD', 'GET'):
+ return True
if request.method == 'POST':
- if self.__class__.model in [ User ]:
- ok = request.user.is_superuser or (request.user.admin_of_organizations.count() > 0)
- if not ok:
- raise PermissionDenied()
- return True
- else:
- # audit all of these to check ownership/readability of subobjects
- if not self.__class__.model.can_user_add(request.user, self.request.DATA):
- raise PermissionDenied()
- return True
- raise exceptions.NotImplementedError
+ if self.__class__.model in [ User ]:
+ ok = request.user.is_superuser or (request.user.admin_of_organizations.count() > 0)
+ if not ok:
+ raise PermissionDenied()
+ return True
+ else:
+ # audit all of these to check ownership/readability of subobjects
+ if not self.__class__.model.can_user_add(request.user, self.request.DATA):
+ raise PermissionDenied()
+ return True
+ return False#raise exceptions.NotImplementedError
def get_queryset(self):
@@ -78,8 +79,8 @@ class BaseSubList(BaseList):
def list_permissions_check(self, request, obj=None):
''' determines some early yes/no access decisions, pre-filtering '''
- if request.method == 'GET':
- return True
+ if request.method in ('OPTIONS', 'HEAD', 'GET'):
+ return True
if request.method == 'POST':
# the can_user_attach methods will be called below
return True
@@ -171,14 +172,10 @@ class BaseSubList(BaseList):
if self.__class__.parent_model == Organization:
organization = Organization.objects.get(pk=request.DATA[inject_primary_key])
import lib.main.views
- if self.__class__ == lib.main.views.OrganizationsUsersList:
+ if self.__class__ == lib.main.views.OrganizationsUsersList:
organization.users.add(obj)
- organization.save()
elif self.__class__ == lib.main.views.OrganizationsAdminsList:
organization.admins.add(obj)
- organization.save()
-
-
else:
if not UserHelper.can_user_read(request.user, obj):
diff --git a/lib/main/models/__init__.py b/lib/main/models/__init__.py
index 977244a355..380a7f06a8 100644
--- a/lib/main/models/__init__.py
+++ b/lib/main/models/__init__.py
@@ -904,6 +904,11 @@ class JobTemplate(CommonModel):
)
return cls.can_user_add(user, data)
+ @classmethod
+ def can_user_administrate(cls, user, obj, data):
+ '''
+ '''
+
@classmethod
def can_user_add(cls, user, data):
'''
@@ -916,6 +921,8 @@ class JobTemplate(CommonModel):
if user.is_superuser:
return True
+ if not data or '_method' in data: # FIXME: So the browseable API will work?
+ return True
project = Project.objects.get(pk=data['project'])
inventory = Inventory.objects.get(pk=data['inventory'])
diff --git a/lib/main/rbac.py b/lib/main/rbac.py
index 079508ebc7..047cf6db06 100644
--- a/lib/main/rbac.py
+++ b/lib/main/rbac.py
@@ -15,58 +15,60 @@
# along with Ansible Commander. If not, see .
-from lib.main.models import *
-from lib.main.serializers import *
-from rest_framework import permissions
-from django.contrib.auth.models import AnonymousUser
-from django.core.exceptions import PermissionDenied
+import logging
from django.http import Http404
+from rest_framework.exceptions import PermissionDenied
+from rest_framework import permissions
+
+logger = logging.getLogger('lib.main.rbac')
# FIXME: this will probably need to be subclassed by object type
class CustomRbac(permissions.BasePermission):
- def _common_user_check(self, request):
- # no anonymous users
- if request.user.is_anonymous():
- # 401, not 403, hence no raised exception
+ def _check_permissions(self, request, view, obj=None):
+ # Check that obj (if given) is active, otherwise raise a 404.
+ active = getattr(obj, 'active', getattr(obj, 'is_active', True))
+ if callable(active):
+ active = active()
+ if not active:
+ raise Http404()
+ # Don't allow anonymous users. 401, not 403, hence no raised exception.
+ if not request.user or request.user.is_anonymous():
return False
- # superusers are always good
- if request.user.is_superuser:
- return True
- # other users must have associated acom user records & be active
+ # Don't allow inactive users (and respond with a 403).
if not request.user.is_active:
raise PermissionDenied()
- return True
-
- def has_permission(self, request, view, obj=None):
- if not self._common_user_check(request):
- return False
+ # Always allow superusers (as long as they are active).
+ if request.user.is_superuser:
+ return True
+ # If no obj is given, check list permissions.
if obj is None:
if getattr(view, 'list_permissions_check', None):
- if request.user.is_superuser:
- return True
if not view.list_permissions_check(request):
raise PermissionDenied()
elif not getattr(view, 'item_permissions_check', None):
- raise Exception("internal error, list_permissions_check or item_permissions_check must be defined")
+ raise Exception('internal error, list_permissions_check or '
+ 'item_permissions_check must be defined')
return True
+ # Otherwise, check the item permissions for the given obj.
else:
- # haven't tested around these confines yet
- raise Exception("did not expect to get to this position")
+ if not view.item_permissions_check(request, obj):
+ raise PermissionDenied()
+ return True
+
+ def has_permission(self, request, view, obj=None):
+ logger.debug('has_permission(user=%s method=%s data=%r, %s, %r)',
+ request.user, request.method, request.DATA,
+ view.__class__.__name__, obj)
+ try:
+ response = self._check_permissions(request, view, obj)
+ except Exception, e:
+ logger.debug('has_permission raised %r', e, exc_info=True)
+ raise
+ else:
+ logger.debug('has_permission returned %r', response)
+ return response
def has_object_permission(self, request, view, obj):
- if isinstance(obj, User):
- if not obj.is_active:
- raise Http404()
- else:
- if not obj.active:
- raise Http404()
- if request.user.is_superuser:
- return True
- if not self._common_user_check(request):
- return False
- if not view.item_permissions_check(request, obj):
- raise PermissionDenied()
- return True
-
+ return self.has_permission(request, view, obj)
diff --git a/lib/main/serializers.py b/lib/main/serializers.py
index cb6b2b8a79..dbe331a427 100644
--- a/lib/main/serializers.py
+++ b/lib/main/serializers.py
@@ -304,6 +304,9 @@ class JobTemplateSerializer(BaseSerializer):
def get_related(self, obj):
# FIXME: fill in once further defined. related resources, credential, project, inventory, etc
res = dict(
+ credential = reverse('main:credentials_detail', args=(obj.credential.pk,)),
+ project = reverse('main:projects_detail', args=(obj.project.pk,)),
+ inventory = reverse('main:inventory_detail', args=(obj.inventory.pk,)),
)
if obj.created_by:
res['created_by'] = reverse('main:users_detail', args=(obj.created_by.pk,))
diff --git a/lib/main/tests/__init__.py b/lib/main/tests/__init__.py
index 937faa473c..5d3e4fb5ff 100644
--- a/lib/main/tests/__init__.py
+++ b/lib/main/tests/__init__.py
@@ -20,5 +20,5 @@ from lib.main.tests.inventory import InventoryTest
from lib.main.tests.projects import ProjectsTest
from lib.main.tests.commands import *
from lib.main.tests.tasks import RunJobTest
-from lib.main.tests.jobs import JobsTest # similar to above, but mostly focused on REST API
+from lib.main.tests.jobs import *
diff --git a/lib/main/tests/base.py b/lib/main/tests/base.py
index 2039557191..34a6c05c94 100644
--- a/lib/main/tests/base.py
+++ b/lib/main/tests/base.py
@@ -14,6 +14,7 @@
# You should have received a copy of the GNU General Public License
# along with Ansible Commander. If not, see .
+import contextlib
import datetime
import json
import os
@@ -21,7 +22,7 @@ import shutil
import tempfile
from django.conf import settings
-from django.contrib.auth.models import User as DjangoUser
+from django.contrib.auth.models import User
import django.test
from django.test.client import Client
from lib.main.models import *
@@ -36,6 +37,8 @@ class BaseTestMixin(object):
super(BaseTestMixin, self).setUp()
self.object_ctr = 0
self._temp_project_dirs = []
+ self._current_auth = None
+ self._user_passwords = {}
def tearDown(self):
super(BaseTestMixin, self).tearDown()
@@ -43,14 +46,30 @@ class BaseTestMixin(object):
if os.path.exists(project_dir):
shutil.rmtree(project_dir, True)
- def make_user(self, username, password, super_user=False):
- django_user = None
+ def make_user(self, username, password=None, super_user=False):
+ user = None
+ password = password or username
if super_user:
- django_user = DjangoUser.objects.create_superuser(username, "%s@example.com", password)
+ user = User.objects.create_superuser(username, "%s@example.com", password)
else:
- django_user = DjangoUser.objects.create_user(username, "%s@example.com", password)
- self.assertTrue(django_user.auth_token)
- return django_user
+ user = User.objects.create_user(username, "%s@example.com", password)
+ self.assertTrue(user.auth_token)
+ self._user_passwords[user.username] = password
+ return user
+
+ @contextlib.contextmanager
+ def current_user(self, user_or_username, password=None):
+ try:
+ if isinstance(user_or_username, User):
+ username = user_or_username.username
+ else:
+ username = user_or_username
+ password = password or self._user_passwords.get(username)
+ previous_auth = self._current_auth
+ self._current_auth = (username, password)
+ yield
+ finally:
+ self._current_auth = previous_auth
def make_organizations(self, created_by, count=1):
results = []
@@ -121,16 +140,17 @@ class BaseTestMixin(object):
def _generic_rest(self, url, data=None, expect=204, auth=None, method=None):
assert method is not None
- method = method.lower()
- if method not in [ 'get', 'delete' ]:
+ method_name = method.lower()
+ if method_name not in ('options', 'head', 'get', 'delete'):
assert data is not None
client = Client()
+ auth = auth or self._current_auth
if auth:
if isinstance(auth, (list, tuple)):
client.login(username=auth[0], password=auth[1])
elif isinstance(auth, basestring):
client = Client(HTTP_AUTHORIZATION='Token %s' % auth)
- method = getattr(client,method)
+ method = getattr(client, method_name)
response = None
if data is not None:
response = method(url, json.dumps(data), 'application/json')
@@ -141,11 +161,19 @@ class BaseTestMixin(object):
assert False, "Failed: %s" % response.content
if expect is not None:
assert response.status_code == expect, "expected status %s, got %s for url=%s as auth=%s: %s" % (expect, response.status_code, url, auth, response.content)
- if response.status_code not in [ 202, 204, 400, 405, 409 ]:
+ if method_name == 'head':
+ self.assertFalse(response.content)
+ if response.status_code not in [ 202, 204, 400, 405, 409 ] and method_name != 'head':
# no JSON responses in these at least for now, 400/409 should probably return some (FIXME)
return json.loads(response.content)
else:
return None
+
+ def options(self, url, expect=200, auth=None):
+ return self._generic_rest(url, data=None, expect=expect, auth=auth, method='options')
+
+ def head(self, url, expect=200, auth=None):
+ return self._generic_rest(url, data=None, expect=expect, auth=auth, method='head')
def get(self, url, expect=200, auth=None):
return self._generic_rest(url, data=None, expect=expect, auth=auth, method='get')
diff --git a/lib/main/tests/jobs.py b/lib/main/tests/jobs.py
index 185630feb4..b443213551 100644
--- a/lib/main/tests/jobs.py
+++ b/lib/main/tests/jobs.py
@@ -16,18 +16,23 @@
import datetime
import json
-
from django.contrib.auth.models import User as DjangoUser
-import django.test
+from django.core.urlresolvers import reverse
from django.test.client import Client
from lib.main.models import *
from lib.main.tests.base import BaseTest
-class JobsTest(BaseTest):
+__all__ = ['JobTemplateTest', 'JobTest']
- def collection(self):
- # not really used
- return '/api/v1/job_templates/'
+TEST_PLAYBOOK = '''- hosts: mygroup
+ gather_facts: false
+ tasks:
+ - name: woohoo
+ command: test 1 = 1
+'''
+
+class BaseJobTest(BaseTest):
+ ''''''
def get_other2_credentials(self):
return ('other2', 'other2')
@@ -36,45 +41,66 @@ class JobsTest(BaseTest):
return ('nobody', 'nobody')
def setUp(self):
- super(JobsTest, self).setUp()
+ super(BaseJobTest, self).setUp()
+
+ # Users
self.setup_users()
+ self.other2_django_user = self.make_user('other2', 'other2')
+ self.nobody_django_user = self.make_user('nobody', 'nobody')
- self.other2_django_user = User.objects.create(username='other2')
- self.other2_django_user.set_password('other2')
- self.other2_django_user.save()
- self.nobody_django_user = User.objects.create(username='nobody')
- self.nobody_django_user.set_password('nobody')
- self.nobody_django_user.save()
-
+ # Organization
self.organization = Organization.objects.create(
- name = 'engineering',
- created_by = self.normal_django_user
- )
-
- self.inventory = Inventory.objects.create(
- name = 'prod',
- organization = self.organization,
- created_by = self.normal_django_user
+ name='engineering',
+ created_by=self.normal_django_user,
)
+ self.organization.admins.add(self.normal_django_user)
+ self.organization.users.add(self.normal_django_user)
+ self.organization.users.add(self.other_django_user)
+ self.organization.users.add(self.other2_django_user)
- self.group_a = Group.objects.create(
- name = 'group1',
- inventory = self.inventory,
- created_by = self.normal_django_user
+ # Team
+ self.team = self.organization.teams.create(
+ name='Tigger',
+ created_by=self.normal_django_user,
)
-
- self.team = Team.objects.create(
- name = 'Tigger',
- created_by = self.normal_django_user
- )
-
self.team.users.add(self.other_django_user)
self.team.users.add(self.other2_django_user)
+ # Project
self.project = self.make_projects(self.normal_django_user, 1,
- playbook_content='')[0]
+ playbook_content=TEST_PLAYBOOK)[0]
self.organization.projects.add(self.project)
+ # Inventory
+ self.inventory = self.organization.inventories.create(
+ name = 'prod',
+ created_by = self.normal_django_user,
+ )
+ self.group_a = self.inventory.groups.create(
+ name = 'group1',
+ created_by = self.normal_django_user
+ )
+ self.host_a = self.inventory.hosts.create(
+ name = '127.0.0.1',
+ created_by = self.normal_django_user
+ )
+ self.host_b = self.inventory.hosts.create(
+ name = '127.0.0.2',
+ created_by = self.normal_django_user
+ )
+ self.group_a.hosts.add(self.host_a)
+ self.group_a.hosts.add(self.host_b)
+
+ # Credentials
+ self.user_credential = self.other_django_user.credentials.create(
+ ssh_key_data = 'xxx',
+ created_by = self.normal_django_user,
+ )
+ self.team_credential = self.team.credentials.create(
+ ssh_key_data = 'xxx',
+ created_by = self.normal_django_user,
+ )
+
# other django user is on the project team and can deploy
self.permission1 = Permission.objects.create(
inventory = self.inventory,
@@ -82,8 +108,7 @@ class JobsTest(BaseTest):
team = self.team,
permission_type = PERM_INVENTORY_DEPLOY,
created_by = self.normal_django_user
- )
-
+ )
# individual permission granted to other2 user, can run check mode
self.permission2 = Permission.objects.create(
inventory = self.inventory,
@@ -93,58 +118,105 @@ class JobsTest(BaseTest):
created_by = self.normal_django_user
)
- self.host_a = Host.objects.create(
- name = '127.0.0.1',
- inventory = self.inventory,
- created_by = self.normal_django_user
- )
-
- self.host_b = Host.objects.create(
- name = '127.0.0.2',
- inventory = self.inventory,
- created_by = self.normal_django_user
- )
-
- self.group_a.hosts.add(self.host_a)
- self.group_a.hosts.add(self.host_b)
- self.group_a.save()
-
-
- self.credential = Credential.objects.create(
- ssh_key_data = 'xxx',
- created_by = self.normal_django_user,
- user = self.other_django_user
- )
-
- self.credential2 = Credential.objects.create(
- ssh_key_data = 'xxx',
- created_by = self.normal_django_user,
- team = self.team,
- )
-
- self.organization.projects.add(self.project)
- self.organization.admins.add(self.normal_django_user)
- self.organization.users.add(self.normal_django_user)
- self.organization.save()
-
- self.template1 = JobTemplate.objects.create(
+ self.job_template1 = JobTemplate.objects.create(
name = 'job-run',
job_type = 'run',
inventory = self.inventory,
- credential = self.credential,
+ credential = self.user_credential,
project = self.project,
+ created_by = self.normal_django_user,
)
- self.template2 = JobTemplate.objects.create(
+ self.job_template2 = JobTemplate.objects.create(
name = 'job-check',
job_type = 'check',
inventory = self.inventory,
- credential = self.credential,
+ credential = self.team_credential,
project = self.project,
+ created_by = self.normal_django_user,
)
+class JobTemplateTest(BaseJobTest):
+
+ def setUp(self):
+ super(JobTemplateTest, self).setUp()
+
+ def test_job_template_list(self):
+ url = reverse('main:job_templates_list')
+
+ response = self.get(url, expect=401)
+ with self.current_user(self.normal_django_user):
+ response = self.get(url, expect=200)
+ self.assertTrue(response['count'], JobTemplate.objects.count())
+
+ # FIXME: Test that user can only see job templates from own organization.
+
+ # org admin can add job template
+ data = dict(
+ name = 'job-foo',
+ credential = self.user_credential.pk,
+ inventory = self.inventory.pk,
+ project = self.project.pk,
+ job_type = PERM_INVENTORY_DEPLOY,
+ )
+ with self.current_user(self.normal_django_user):
+ response = self.post(url, data, expect=201)
+ detail_url = reverse('main:job_templates_detail',
+ args=(response['id'],))
+ self.assertEquals(response['url'], detail_url)
+
+ # other_django_user is on a team that can deploy, so can create both
+ # deploy and check type job templates
+ with self.current_user(self.other_django_user):
+ data['name'] = 'job-foo2'
+ response = self.post(url, data, expect=201)
+ data['name'] = 'job-foo3'
+ data['job_type'] = PERM_INVENTORY_CHECK
+ response = self.post(url, data, expect=201)
+
+ # other2_django_user has individual permissions to run check mode,
+ # but not deploy
+ with self.current_user(self.other2_django_user):
+ data['name'] = 'job-foo4'
+ #data['credential'] = self.user_credential.pk
+ #response = self.post(url, data, expect=201)
+ data['name'] = 'job-foo5'
+ data['job_type'] = PERM_INVENTORY_DEPLOY
+ response = self.post(url, data, expect=403)
+
+ # nobody user can't even run check mode
+ with self.current_user(self.nobody_django_user):
+ data['name'] = 'job-foo5'
+ data['job_type'] = PERM_INVENTORY_CHECK
+ response = self.post(url, data, expect=403)
+ data['job_type'] = PERM_INVENTORY_DEPLOY
+ response = self.post(url, data, expect=403)
+
+ def test_job_template_detail(self):
+
+ return # FIXME
+ # verify we can also get the job template record
+ got = self.get(url, expect=200, auth=self.get_other2_credentials())
+ self.failUnlessEqual(got['url'], '/api/v1/job_templates/6/')
+
+ # TODO: add more tests that show
+ # the method used to START a JobTemplate follow the exact same permissions as those to create it ...
+ # and that jobs come back nicely serialized with related resources and so on ...
+ # that we can drill all the way down and can get at host failure lists, etc ...
+
+
+
+
+
+class JobTest(BaseJobTest):
+
+ def setUp(self):
+ super(JobTest, self).setUp()
+
def test_mainline(self):
+ return # FIXME
+
# job templates
data = self.get('/api/v1/job_templates/', expect=401)
data = self.get('/api/v1/job_templates/', expect=200, auth=self.get_normal_credentials())
diff --git a/lib/main/tests/organizations.py b/lib/main/tests/organizations.py
index 3f9a595500..11388dbf25 100644
--- a/lib/main/tests/organizations.py
+++ b/lib/main/tests/organizations.py
@@ -18,6 +18,7 @@ import datetime
import json
from django.contrib.auth.models import User as DjangoUser
+from django.core.urlresolvers import reverse
import django.test
from django.test.client import Client
from lib.main.models import *
@@ -64,17 +65,26 @@ class OrganizationsTest(BaseTest):
self.organizations[1].admins.add(self.normal_django_user)
def test_get_list(self):
+ url = reverse('main:organizations_list')
# no credentials == 401
- self.get(self.collection(), expect=401)
+ self.options(url, expect=401)
+ self.head(url, expect=401)
+ self.get(url, expect=401)
# wrong credentials == 401
- self.get(self.collection(), expect=401, auth=self.get_invalid_credentials())
+ with self.current_user(self.get_invalid_credentials()):
+ self.options(url, expect=401)
+ self.head(url, expect=401)
+ self.get(url, expect=401)
# superuser credentials == 200, full list
- data = self.get(self.collection(), expect=200, auth=self.get_super_credentials())
- self.check_pagination_and_size(data, 10, previous=None, next=None)
- [self.assertTrue(key in data['results'][0]) for key in ['name', 'description', 'url', 'creation_date', 'id' ]]
+ with self.current_user(self.super_django_user):
+ self.options(url, expect=200)
+ self.head(url, expect=200)
+ data = self.get(url, expect=200)
+ self.check_pagination_and_size(data, 10, previous=None, next=None)
+ [self.assertTrue(key in data['results'][0]) for key in ['name', 'description', 'url', 'creation_date', 'id' ]]
# check that the related URL functionality works
related = data['results'][0]['related']
diff --git a/lib/main/views.py b/lib/main/views.py
index cf1034ee3e..bec014d48d 100644
--- a/lib/main/views.py
+++ b/lib/main/views.py
@@ -20,8 +20,8 @@ from lib.main.models import *
from django.contrib.auth.models import User
from lib.main.serializers import *
from lib.main.rbac import *
-from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import reverse
+from rest_framework.exceptions import PermissionDenied
from rest_framework import mixins
from rest_framework import generics
from rest_framework import permissions
@@ -266,7 +266,7 @@ class TeamsList(BaseList):
if self.request.user.is_superuser:
return base.all()
return base.filter(
- admins__in = [ self.request.user ]
+ organization__admins__in = [ self.request.user ]
).distinct() | base.filter(
users__in = [ self.request.user ]
).distinct()
diff --git a/lib/settings/defaults.py b/lib/settings/defaults.py
index 378e340d83..6ce341fc4c 100644
--- a/lib/settings/defaults.py
+++ b/lib/settings/defaults.py
@@ -187,3 +187,54 @@ CELERYD_TASK_TIME_LIMIT = 3600
CELERYD_TASK_SOFT_TIME_LIMIT = 3540
CELERYBEAT_SCHEDULER = 'djcelery.schedulers.DatabaseScheduler'
CELERYBEAT_MAX_LOOP_INTERVAL = 60
+
+LOGGING = {
+ 'version': 1,
+ 'disable_existing_loggers': False,
+ 'filters': {
+ 'require_debug_false': {
+ '()': 'django.utils.log.RequireDebugFalse',
+ },
+ 'require_debug_true': {
+ '()': 'django.utils.log.RequireDebugTrue',
+ },
+ },
+ 'handlers': {
+ 'console': {
+ 'level': 'DEBUG',
+ #'filters': ['require_debug_true'],
+ 'class': 'logging.StreamHandler',
+ },
+ 'null': {
+ 'class': 'django.utils.log.NullHandler',
+ },
+ 'mail_admins': {
+ 'level': 'ERROR',
+ 'filters': ['require_debug_false'],
+ 'class': 'django.utils.log.AdminEmailHandler'
+ }
+ },
+ 'loggers': {
+ 'django': {
+ 'handlers': ['console'],
+ },
+ 'django.request': {
+ 'handlers': ['mail_admins'],
+ 'level': 'ERROR',
+ 'propagate': False,
+ },
+ 'py.warnings': {
+ 'handlers': ['console'],
+ },
+ 'lib.main': {
+ 'handlers': ['console'],
+ 'level': 'DEBUG',
+ 'filters': []
+ },
+ 'lib.main.rbac': {
+ 'handlers': ['null'],
+ # Comment the line below to show lots of permissions logging.
+ 'propagate': False,
+ },
+ }
+}