Commit dfb15f98 by Őry Máté

Merge remote-tracking branch 'origin/acl'

parents be07eae3 6b43a7c3
......@@ -18,6 +18,6 @@ _build
# Logs:
*.log
.ropeproject
#celery
celerybeat-schedule
.coverage
*,cover
"""
Creates Levels for all installed apps that have levels.
"""
from django.db.models import get_models, signals
from django.db import DEFAULT_DB_ALIAS
from django.core.exceptions import ImproperlyConfigured
from ..models import Level, AclBase
def create_levels(app, created_models, verbosity, db=DEFAULT_DB_ALIAS,
**kwargs):
"""Create and set the weights of the configured Levels.
Based on django.contrib.auth.management.__init__.create_permissions"""
# if not router.allow_migrate(db, auth_app.Permission):
# return
from django.contrib.contenttypes.models import ContentType
app_models = [k for k in get_models(app) if AclBase in k.__bases__]
print "Creating levels for models: %s." % ", ".join([m.__name__ for m in app_models])
# This will hold the levels we're looking for as
# (content_type, (codename, name))
searched_levels = list()
level_weights = list()
# The codenames and ctypes that should exist.
ctypes = set()
for klass in app_models:
# Force looking up the content types in the current database
# before creating foreign keys to them.
ctype = ContentType.objects.db_manager(db).get_for_model(klass)
ctypes.add(ctype)
weight = 0
try:
for codename, name in klass.ACL_LEVELS:
searched_levels.append((ctype, (codename, name)))
level_weights.append((ctype, codename, weight))
weight += 1
except AttributeError:
raise ImproperlyConfigured(
"Class %s doesn't have ACL_LEVELS attribute." % klass)
# Find all the Levels that have a content_type for a model we're
# looking for. We don't need to check for codenames since we already have
# a list of the ones we're going to create.
all_levels = set(Level.objects.using(db).filter(
content_type__in=ctypes,
).values_list(
"content_type", "codename"
))
levels = [
Level(codename=codename, name=name, content_type=ctype)
for ctype, (codename, name) in searched_levels
if (ctype.pk, codename) not in all_levels
]
Level.objects.using(db).bulk_create(levels)
if verbosity >= 2:
print("Adding levels [%s]." % ", ".join(levels))
print("Searched: [%s]." % ", ".join([unicode(l) for l in searched_levels]))
print("All: [%s]." % ", ".join([unicode(l) for l in all_levels]))
# set weights
for ctype, codename, weight in level_weights:
Level.objects.filter(codename=codename,
content_type=ctype).update(weight=weight)
signals.post_syncdb.connect(
create_levels, dispatch_uid="circle.acl.management.create_levels")
from django.core.management.base import BaseCommand
from .. import create_levels
class Command(BaseCommand):
args = ''
help = 'Regenerates Levels'
def handle(self, *args, **options):
create_levels(None, None, 3)
import logging
from django.contrib.auth.models import User, Group
from django.contrib.contenttypes.generic import (
GenericForeignKey, GenericRelation
)
from django.contrib.contenttypes.models import ContentType
from django.db.models import (
ManyToManyField, ForeignKey, CharField, Model, IntegerField
)
logger = logging.getLogger(__name__)
class Level(Model):
"""Definition of a permission level.
Instances are automatically populated based on AclBase."""
name = CharField('name', max_length=50)
content_type = ForeignKey(ContentType)
codename = CharField('codename', max_length=100)
weight = IntegerField('weight', null=True)
def __unicode__(self):
return "<%s/%s>" % (unicode(self.content_type), self.name)
class Meta:
unique_together = (('content_type', 'codename'),
# ('content_type', 'weight'),
# TODO find a way of temp. disabling this constr.
)
class ObjectLevel(Model):
"""Permission level for a specific object."""
level = ForeignKey(Level)
content_type = ForeignKey(ContentType)
object_id = CharField(max_length=255)
content_object = GenericForeignKey()
users = ManyToManyField(User)
groups = ManyToManyField(Group)
def __unicode__(self):
return "<%s: %s>" % (unicode(self.content_object), unicode(self.level))
class Meta:
unique_together = (('content_type', 'object_id', 'level'),)
class AclBase(Model):
"""Define permission levels for Users/Groups per object."""
object_level_set = GenericRelation(ObjectLevel)
@classmethod
def get_level_object(cls, level):
"""Get Level object for this model by codename."""
ct = ContentType.objects.get_for_model(cls)
return Level.objects.get(codename=level, content_type=ct)
def set_level(self, whom, level):
"""Set level of object for a user or group.
:param whom: user or group the level is set for
:type whom: User or Group
:param level: codename of level to set
:type level: Level or str or unicode
"""
if isinstance(whom, User):
self.set_user_level(whom, level)
elif isinstance(whom, Group):
self.set_group_level(whom, level)
else:
raise AttributeError('"whom" must be a User or Group object.')
def set_user_level(self, user, level):
"""Set level of object for a user.
:param whom: user the level is set for
:type whom: User
:param level: codename of level to set
:type level: Level or str or unicode
"""
logger.info('%s.set_user_level(%s, %s) called',
*[unicode(p) for p in [self, user, level]])
if isinstance(level, basestring):
level = self.get_level_object(level)
if not self.object_level_set.filter(level_id=level.pk).exists():
self.object_level_set.create(level=level)
for i in self.object_level_set.all():
if i.level_id != level.pk:
i.users.remove(user)
else:
i.users.add(user)
i.save()
def set_group_level(self, group, level):
"""Set level of object for a user.
:param whom: user the level is set for
:type whom: User or unicode or str
:param level: codename of level to set
:type level: str or unicode
"""
logger.info('%s.set_group_level(%s, %s) called',
*[unicode(p) for p in [self, group, level]])
if isinstance(level, basestring):
level = self.get_level_object(level)
#self.object_level_set.get_or_create(level=level, content_object=self)
if not self.object_level_set.filter(level_id=level.pk).exists():
self.object_level_set.create(level=level)
for i in self.object_level_set.all():
if i.level_id != level.pk:
i.groups.remove(group)
else:
i.groups.add(group)
i.save()
def has_level(self, user, level, group_also=True):
logger.debug('%s.has_level(%s, %s, %s) called',
*[unicode(p) for p in [self, user, level, group_also]])
if getattr(user, 'is_superuser', False):
logger.debug('- superuser granted')
return True
if isinstance(level, basestring):
level = self.get_level_object(level)
logger.debug("- level set by str: %s", unicode(level))
object_levels = self.object_level_set.filter(
level__weight__gte=level.weight).all()
groups = user.groups.values_list('id', flat=True) if group_also else []
for i in object_levels:
if i.users.filter(pk=user.pk).exists():
return True
if group_also and i.groups.filter(pk__in=groups).exists():
return True
return False
def get_users_with_level(self):
logger.debug('%s.get_users_with_level() called', unicode(self))
object_levels = (self.object_level_set.select_related(
'users', 'level').all())
users = []
for object_level in object_levels:
name = object_level.level.codename
olusers = object_level.users.all()
users.extend([(u, name) for u in olusers])
logger.debug('- %s: %s' % (name, [u.username for u in olusers]))
return users
def get_groups_with_level(self):
logger.debug('%s.get_groups_with_level() called', unicode(self))
object_levels = (self.object_level_set.select_related(
'groups', 'level').all())
groups = []
for object_level in object_levels:
name = object_level.level.codename
olgroups = object_level.groups.all()
groups.extend([(g, name) for g in olgroups])
logger.debug('- %s: %s' % (name, [g.name for g in olgroups]))
return groups
class Meta:
abstract = True
from django.db.models import TextField
from ..models import AclBase
class TestModel(AclBase):
normal_field = TextField()
ACL_LEVELS = (
('alfa', 'Alfa'),
('bravo', 'Bravo'),
('charlie', 'Charlie'),
)
class Test2Model(AclBase):
normal2_field = TextField()
ACL_LEVELS = (
('one', 'One'),
('two', 'Two'),
('three', 'Three'),
)
from django.test import TestCase
from django.contrib.auth.models import User, Group, AnonymousUser
from ..models import ObjectLevel
from .models import TestModel, Test2Model
class AclUserTest(TestCase):
def setUp(self):
self.u1 = User.objects.create(username='user1')
self.u2 = User.objects.create(username='user2', is_staff=True)
self.us = User.objects.create(username='superuser', is_superuser=True)
self.g1 = Group.objects.create(name='group1')
self.g1.user_set.add(self.u1)
self.g1.user_set.add(self.u2)
self.g1.save()
def test_level_exists(self):
for codename, name in TestModel.ACL_LEVELS:
level = TestModel.get_level_object(codename)
self.assertEqual(level.codename, codename)
for codename, name in Test2Model.ACL_LEVELS:
level = Test2Model.get_level_object(codename)
self.assertEqual(level.codename, codename)
def test_lowest_user_level(self):
i = TestModel.objects.create(normal_field='Hello')
self.assertFalse(i.has_level(self.u1, 'alfa', False))
self.assertFalse(i.has_level(self.u1, 'bravo', False))
i.set_level(self.u1, 'alfa')
i.set_level(self.g1, 'bravo')
self.assertTrue(i.has_level(self.u1, 'alfa', False))
self.assertFalse(i.has_level(self.u1, 'bravo', False))
def test_anonymous_user_level(self):
i = TestModel.objects.create(normal_field='Hello')
anon = AnonymousUser()
self.assertFalse(i.has_level(anon, 'alfa'))
self.assertFalse(i.has_level(anon, 'bravo'))
def test_middle_user_level(self):
i = TestModel.objects.create(normal_field='Hello')
self.assertFalse(i.has_level(self.u1, 'alfa'))
self.assertFalse(i.has_level(self.u1, 'bravo'))
self.assertFalse(i.has_level(self.u1, 'charlie'))
i.set_level(self.u1, 'bravo')
self.assertTrue(i.has_level(self.u1, 'alfa'))
self.assertTrue(i.has_level(self.u1, 'bravo'))
self.assertFalse(i.has_level(self.u1, 'charlie'))
def test_level_set_twice_same(self):
i = TestModel.objects.create(normal_field='Hello')
self.assertFalse(i.has_level(self.u1, 'alfa'))
self.assertFalse(i.has_level(self.u1, 'bravo'))
self.assertFalse(i.has_level(self.u1, 'charlie'))
i.set_level(self.u1, 'bravo')
i.set_level(self.u1, 'bravo')
self.assertTrue(i.has_level(self.u1, 'alfa'))
self.assertTrue(i.has_level(self.u1, 'bravo'))
self.assertFalse(i.has_level(self.u1, 'charlie'))
def test_level_set_twice_different(self):
i = TestModel.objects.create(normal_field='Hello')
self.assertFalse(i.has_level(self.u1, 'alfa'))
self.assertFalse(i.has_level(self.u1, 'bravo'))
self.assertFalse(i.has_level(self.u1, 'charlie'))
i.set_level(self.u1, 'charlie')
i.set_level(self.u1, 'bravo')
self.assertTrue(i.has_level(self.u1, 'alfa'))
self.assertTrue(i.has_level(self.u1, 'bravo'))
self.assertFalse(i.has_level(self.u1, 'charlie'))
def test_superuser(self):
i = TestModel.objects.create(normal_field='Hello')
for u, v in [(self.u1, False), (self.u2, False), (self.us, True)]:
self.assertEqual(i.has_level(u, 'alfa'), v)
self.assertEqual(i.has_level(u, 'bravo'), v)
self.assertEqual(i.has_level(u, 'charlie'), v)
def test_check_group_membership(self):
groups = self.u1.groups.values_list('id', flat=True)
self.assertIn(self.g1.id, groups)
self.assertTrue(self.g1.user_set.filter(id=self.u2.id).exists())
def test_lowest_group_level(self):
i = TestModel.objects.create(normal_field='Hello')
self.assertFalse(i.has_level(self.u1, 'alfa'))
self.assertFalse(i.has_level(self.u1, 'bravo'))
i.set_level(self.g1, 'alfa')
self.assertTrue(i.has_level(self.u1, 'alfa'))
self.assertFalse(i.has_level(self.u1, 'bravo'))
def test_middle_group_level(self):
i = TestModel.objects.create(normal_field='Hello')
self.assertFalse(i.has_level(self.u1, 'alfa'))
self.assertFalse(i.has_level(self.u1, 'bravo'))
self.assertFalse(i.has_level(self.u1, 'charlie'))
i.set_level(self.g1, 'bravo')
self.assertTrue(i.has_level(self.u1, 'alfa'))
self.assertTrue(i.has_level(self.u1, 'bravo'))
self.assertFalse(i.has_level(self.u1, 'charlie'))
def test_set_level_error_handling(self):
with self.assertRaises(AttributeError):
TestModel.objects.create().set_level('wrong arg', 'level')
def test_get_users_with_level(self):
i1 = TestModel.objects.create(normal_field='Hello')
i2 = Test2Model.objects.create(normal2_field='Hello2')
i1.set_level(self.u1, 'bravo')
i1.set_level(self.u2, 'charlie')
i2.set_level(self.u1, 'one')
i2.set_level(self.us, u'three')
res1 = i1.get_users_with_level()
self.assertEqual([(self.u1, u'bravo'), (self.u2, u'charlie')], res1)
res2 = i2.get_users_with_level()
self.assertEqual([(self.u1, u'one'), (self.us, u'three')], res2)
def test_get_groups_with_level(self):
i1 = TestModel.objects.create(normal_field='Hello')
i2 = Test2Model.objects.create(normal2_field='Hello2')
i1.set_level(self.g1, 'bravo')
i1.set_level(self.u2, 'charlie')
i2.set_level(self.g1, 'one')
i2.set_level(self.us, u'three')
res1 = i1.get_groups_with_level()
self.assertEqual([(self.g1, u'bravo')], res1)
res2 = i2.get_groups_with_level()
self.assertEqual([(self.g1, u'one')], res2)
def test_object_level_unicode(self):
i1 = TestModel.objects.create(normal_field='Hello')
i1.set_level(self.g1, 'bravo')
unicode(ObjectLevel.objects.all()[0])
# Create your views here.
"""Common settings and globals."""
from datetime import timedelta
from os import environ
from os.path import abspath, basename, dirname, join, normpath
from json import loads
# from socket import SOCK_STREAM
from sys import path
# Normally you should not import ANYTHING from Django directly
# into your settings, but ImproperlyConfigured is an exception.
from django.core.exceptions import ImproperlyConfigured
......@@ -236,6 +237,7 @@ LOCAL_APPS = (
'network',
'dashboard',
'manager',
'acl',
)
# See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
......@@ -306,5 +308,12 @@ VM_ACCESS_PROTOCOLS = loads(get_env_variable('DJANGO_VM_ACCESS_PROTOCOLS',
"ssh": ["SSH", 22, "tcp"]}'''))
VM_SCHEDULER = 'manager.scheduler'
BROKER_URL = get_env_variable('AMQP_URI')
BROKER_URL=get_env_variable('AMQP_URI')
# Set up periodic firewall tasks
CELERYBEAT_SCHEDULE = {
'blabla': {
'task': 'firewall.tasks.local_tasks.periodic_task',
'schedule': timedelta(seconds=5),
},
}
......@@ -16,3 +16,9 @@ DATABASES = {
"PORT": "",
},
}
SOUTH_TESTS_MIGRATE = False
INSTALLED_APPS += (
'acl.tests',
)
{% load i18n %}<!DOCTYPE html>
<html lang="{{lang}}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="">
<meta name="author" content="">
<title>{% block title %}{% block title-page %}{% endblock %} | {% block title-site %}Circle{% endblock %}{% endblock %}</title>
<script src="//code.jquery.com/jquery-1.10.2.min.js"></script>
<script src="//code.jquery.com/jquery-migrate-1.2.1.min.js"></script>
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css">
<script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script>
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap-theme.min.css">
<link href="//netdna.bootstrapcdn.com/font-awesome/3.2.1/css/font-awesome.css" rel="stylesheet">
<script src="{{ STATIC_URL }}dashboard/js/jquery.knob.js"></script>
<script src="{{ STATIC_URL}}dashboard/bootstrap-slider/bootstrap-slider.js"></script>
<link rel="stylesheet" href="{{ STATIC_URL }}dashboard/bootstrap-slider/slider.css"/>
<link href="{{ STATIC_URL }}dashboard/dashboard.css" rel="stylesheet">
<script src="{{ STATIC_URL }}dashboard/dashboard.js"></script>
</head>
<body>
<div class="navbar navbar-inverse navbar-fixed-top">
<a class="navbar-brand" href="/dashboard/">{% block header-site %}Circle{% endblock %}</a>
</div>
<div class="container">
{% block content %}
<h1 class="alert alert-error">Please override "content" block.</h1>
{% endblock %}
<hr />
<footer>
<p>&copy; Company 2013</p>
</footer>
</div> <!-- /container -->
</body>
{% block extra_js %}
{% endblock %}
</html>
......@@ -5,16 +5,40 @@
<a href="#" class="btn btn-link">{% trans "Transfer ownership..." %}</a>
</p>
<h3>{% trans "Permissions"|capfirst %}</h3>
<form action="{{acl.url}}" method="post">{% csrf_token %}
<table class="table table-striped table-with-form-fields">
<thead><tr><th></th><th>{% trans "Who" %}</th><th>{% trans "What" %}</th><th></th></tr></thead>
<tbody>
<tr><td><i class="icon-user"></i></td><td>NEP123 (Gipsz Jakab)</td><td><select class="form-control"><option>owner</option></select></td><td><a href="#" class="btn btn-link btn-xs"><i class="icon-remove"><span class="sr-only">{% trans "remove" %}</span></i></a></td></tr>
<tr><td><i class="icon-user"></i></td><td>NEP123 (Gipsz Jakab)</td><td><select class="form-control"><option>control</option></select></td><td><a href="#" class="btn btn-link btn-xs"><i class="icon-remove"><span class="sr-only">{% trans "remove" %}</span></i></a></td></tr>
<tr><td><i class="icon-group"></i></td><td>Cloud-fejlesztők</td><td><select class="form-control"><option>view</option></select></td><td><a href="#" class="btn btn-link btn-xs"><i class="icon-remove"><span class="sr-only">{% trans "remove" %}</span></i></a></td></tr>
<tr><td><i class="icon-plus"></i></td><td><input type="text" class="form-control"></td>
<td><select class="form-control"><option>owner</option></select></td><td></td></tr>
{% for i in acl.users %}
<tr><td><i class="icon-user"></i></td><td>{{i.user}}</td>
<td><select class="form-control" name="perm-u-{{i.user.id}}">
{% for id, name in acl.levels %}
<option{%if id = i.level%} selected="selected"{%endif%} value="{{id}}">{{name}}</option>
{% endfor %}
</select></td>
<td><a href="#" class="btn btn-link btn-xs"><i class="icon-remove"><span class="sr-only">{% trans "remove" %}</span></i></a></td></tr>
{% endfor %}
{% for i in acl.groups %}
<tr><td><i class="icon-group"></i></td><td>{{i.group}}</td>
<td><select class="form-control" name="perm-g-{{i.group.id}}">
{% for id, name in acl.levels %}
<option{%if id = i.level%} selected="selected"{%endif%} value="{{id}}">{{name}}</option>
{% endfor %}
</select></td>
<td><a href="#" class="btn btn-link btn-xs"><i class="icon-remove"><span class="sr-only">{% trans "remove" %}</span></i></a></td></tr>
{% endfor %}
<tr><td><i class="icon-plus"></i></td>
<td><input type="text" class="form-control" name="perm-new-name"
placeholder="{% trans "Name of group or user" %}"></td>
<td><select class="form-control" name="perm-new">
{% for id, name in acl.levels %}
<option value="{{id}}">{{name}}</option>
{% endfor %}
</select></td><td></td>
</tr>
</tbody>
</table>
<div class="form-actions">
<button type="submit" class="btn btn-success">{% trans "Save" %}</button>
</div>
</form>
"""
This file demonstrates writing tests using the unittest module. These will pass
when you run "manage.py test".
Replace this with more appropriate tests for your application.
"""
from django.test import TestCase
class SimpleTest(TestCase):
def test_basic_addition(self):
"""
Tests that 1 + 1 always equals 2.
"""
self.assertEqual(1 + 1, 2)
from django.test import TestCase
from django.test.client import Client
from django.contrib.auth.models import User, Group
class VmDetailTest(TestCase):
fixtures = ['test-vm-fixture.json']
def setUp(self):
self.u1 = User.objects.create(username='user1')
self.u2 = User.objects.create(username='user2', is_staff=True)
self.us = User.objects.create(username='superuser', is_superuser=True)
self.g1 = Group.objects.create(name='group1')
self.g1.user_set.add(self.u1)
self.g1.user_set.add(self.u2)
self.g1.save()
def test_404_vm_page(self):
c = Client()
response = c.get('/dashboard/vm/235555/')
self.assertEqual(response.status_code, 404)
def test_vm_page(self):
c = Client()
response = c.get('/dashboard/vm/1/')
self.assertEqual(response.status_code, 200)
from django.conf.urls import patterns, url
from .views import IndexView, VmDetailView, VmList, VmCreate, TemplateDetail
from vm.models import Instance
from .views import (
IndexView, VmDetailView, VmList, VmCreate, TemplateDetail, AclUpdateView
)
urlpatterns = patterns(
'',
......@@ -9,6 +12,8 @@ urlpatterns = patterns(
name='dashboard.views.template-detail'),
url(r'^vm/(?P<pk>\d+)/$', VmDetailView.as_view(),
name='dashboard.views.detail'),
url(r'^vm/(?P<pk>\d+)/acl/$', AclUpdateView.as_view(model=Instance),
name='dashboard.views.vm-acl'),
url(r'^vm/list/$', VmList.as_view(), name='dashboard.views.vm-list'),
url(r'^vm/create/$', VmCreate.as_view(),
name='dashboard.views.vm-create'),
......
from os import getenv
import json
import logging
import re
from django.contrib.auth.models import User, Group
from django.contrib.messages import warning
from django.core import signing
from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import reverse
from django.shortcuts import redirect
from django.utils.translation import ugettext_lazy as _
from django.views.generic import TemplateView, DetailView, View
from django.views.generic.detail import SingleObjectMixin
from django.http import HttpResponse
from django.views.generic import TemplateView, DetailView
from django.core.urlresolvers import reverse_lazy
......@@ -6,13 +20,12 @@ from django.shortcuts import redirect
from django_tables2 import SingleTableView
from tables import VmListTable
from .tables import VmListTable
from vm.models import Instance, InstanceTemplate, InterfaceTemplate
from firewall.models import Vlan
from storage.models import Disk
from django.core import signing
from os import getenv
import json
logger = logging.getLogger(__name__)
class IndexView(TemplateView):
......@@ -40,12 +53,22 @@ class IndexView(TemplateView):
return context
def get_acl_data(obj):
levels = obj.ACL_LEVELS
users = obj.get_users_with_level()
users = [{'user': u, 'level': l} for u, l in users]
groups = obj.get_groups_with_level()
groups = [{'group': g, 'level': l} for g, l in groups]
return {'users': users, 'groups': groups, 'levels': levels,
'url': reverse('dashboard.views.vm-acl', args=[obj.pk])}
class VmDetailView(DetailView):
template_name = "dashboard/vm-detail.html"
queryset = Instance.objects.all()
model = Instance
def get_context_data(self, **kwargs):
context = super(DetailView, self).get_context_data(**kwargs)
context = super(VmDetailView, self).get_context_data(**kwargs)
instance = context['instance']
if instance.node:
port = instance.vnc_port
......@@ -56,9 +79,55 @@ class VmDetailView(DetailView):
context.update({
'vnc_url': '%s' % value
})
context['acl'] = get_acl_data(instance)
return context
class AclUpdateView(View, SingleObjectMixin):
def post(self, request, *args, **kwargs):
instance = self.get_object()
if not (instance.has_level(request.user, "owner") or
getattr(instance, 'owner', None) == request.user):
logger.warning('Tried to set permissions of %s by non-owner %s.',
unicode(instance), unicode(request.user))
raise PermissionDenied()
self.set_levels(request, instance)
self.add_levels(request, instance)
return redirect(instance)
def set_levels(self, request, instance):
for key, value in request.POST.items():
m = re.match('perm-([ug])-(\d+)', key)
if m:
type, id = m.groups()
entity = {'u': User, 'g': Group}[type].objects.get(id=id)
instance.set_level(entity, value)
logger.info("Set %s's acl level for %s to %s by %s.",
unicode(entity), unicode(instance),
value, unicode(request.user))
def add_levels(self, request, instance):