diff --git a/.gitignore b/.gitignore index ebf617f..51ce58d 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,6 @@ _build # Logs: *.log .ropeproject - -#celery celerybeat-schedule +.coverage +*,cover diff --git a/circle/acl/__init__.py b/circle/acl/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/circle/acl/__init__.py diff --git a/circle/acl/management/__init__.py b/circle/acl/management/__init__.py new file mode 100644 index 0000000..d44b240 --- /dev/null +++ b/circle/acl/management/__init__.py @@ -0,0 +1,72 @@ +""" +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") diff --git a/circle/acl/management/commands/__init__.py b/circle/acl/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/circle/acl/management/commands/__init__.py diff --git a/circle/acl/management/commands/update_levels.py b/circle/acl/management/commands/update_levels.py new file mode 100644 index 0000000..0a0a47f --- /dev/null +++ b/circle/acl/management/commands/update_levels.py @@ -0,0 +1,10 @@ +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) diff --git a/circle/acl/models.py b/circle/acl/models.py new file mode 100644 index 0000000..613a3d2 --- /dev/null +++ b/circle/acl/models.py @@ -0,0 +1,170 @@ +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 diff --git a/circle/acl/tests/__init__.py b/circle/acl/tests/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/circle/acl/tests/__init__.py diff --git a/circle/acl/tests/models.py b/circle/acl/tests/models.py new file mode 100644 index 0000000..1b2c522 --- /dev/null +++ b/circle/acl/tests/models.py @@ -0,0 +1,23 @@ +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'), + ) diff --git a/circle/acl/tests/test_acl.py b/circle/acl/tests/test_acl.py new file mode 100644 index 0000000..2fbc14e --- /dev/null +++ b/circle/acl/tests/test_acl.py @@ -0,0 +1,135 @@ +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]) diff --git a/circle/acl/views.py b/circle/acl/views.py new file mode 100644 index 0000000..60f00ef --- /dev/null +++ b/circle/acl/views.py @@ -0,0 +1 @@ +# Create your views here. diff --git a/circle/circle/settings/base.py b/circle/circle/settings/base.py index 54ff82b..8392f4b 100644 --- a/circle/circle/settings/base.py +++ b/circle/circle/settings/base.py @@ -1,12 +1,13 @@ """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), + }, +} diff --git a/circle/circle/settings/test.py b/circle/circle/settings/test.py index 3d29b0c..49fccc6 100644 --- a/circle/circle/settings/test.py +++ b/circle/circle/settings/test.py @@ -16,3 +16,9 @@ DATABASES = { "PORT": "", }, } +SOUTH_TESTS_MIGRATE = False + + +INSTALLED_APPS += ( + 'acl.tests', +) diff --git a/circle/dashboard/fixtures/test-vm-fixture.json b/circle/dashboard/fixtures/test-vm-fixture.json new file mode 100644 index 0000000..2409a13 --- /dev/null +++ b/circle/dashboard/fixtures/test-vm-fixture.json @@ -0,0 +1,1405 @@ +[ +{ + "pk": 1, + "model": "sites.site", + "fields": { + "domain": "example.com", + "name": "example.com" + } +}, +{ + "pk": 1, + "model": "vm.lease", + "fields": { + "suspend_interval_seconds": 18000, + "name": "lab", + "delete_interval_seconds": 180000 + } +}, +{ + "pk": 1, + "model": "storage.datastore", + "fields": { + "path": "/disks", + "hostname": "wut", + "name": "diszkek" + } +}, +{ + "pk": 1, + "model": "storage.disk", + "fields": { + "name": "diszk", + "created": "2013-09-16T09:05:56.926Z", + "modified": "2013-09-19T09:10:25.117Z", + "filename": "disc.img", + "destroyed": null, + "base": null, + "ready": true, + "datastore": 1, + "dev_num": "a", + "type": "raw-rw", + "size": 8589934592 + } +}, +{ + "pk": 1, + "model": "acl.level", + "fields": { + "codename": "owner", + "weight": 2, + "name": "owner", + "content_type": 15 + } +}, +{ + "pk": 2, + "model": "acl.level", + "fields": { + "codename": "operator", + "weight": 1, + "name": "operator", + "content_type": 15 + } +}, +{ + "pk": 3, + "model": "acl.level", + "fields": { + "codename": "user", + "weight": 0, + "name": "user", + "content_type": 15 + } +}, +{ + "pk": 1, + "model": "auth.permission", + "fields": { + "codename": "add_permission", + "name": "Can add permission", + "content_type": 1 + } +}, +{ + "pk": 2, + "model": "auth.permission", + "fields": { + "codename": "change_permission", + "name": "Can change permission", + "content_type": 1 + } +}, +{ + "pk": 3, + "model": "auth.permission", + "fields": { + "codename": "delete_permission", + "name": "Can delete permission", + "content_type": 1 + } +}, +{ + "pk": 4, + "model": "auth.permission", + "fields": { + "codename": "add_group", + "name": "Can add group", + "content_type": 2 + } +}, +{ + "pk": 5, + "model": "auth.permission", + "fields": { + "codename": "change_group", + "name": "Can change group", + "content_type": 2 + } +}, +{ + "pk": 6, + "model": "auth.permission", + "fields": { + "codename": "delete_group", + "name": "Can delete group", + "content_type": 2 + } +}, +{ + "pk": 7, + "model": "auth.permission", + "fields": { + "codename": "add_user", + "name": "Can add user", + "content_type": 3 + } +}, +{ + "pk": 8, + "model": "auth.permission", + "fields": { + "codename": "change_user", + "name": "Can change user", + "content_type": 3 + } +}, +{ + "pk": 9, + "model": "auth.permission", + "fields": { + "codename": "delete_user", + "name": "Can delete user", + "content_type": 3 + } +}, +{ + "pk": 10, + "model": "auth.permission", + "fields": { + "codename": "add_contenttype", + "name": "Can add content type", + "content_type": 4 + } +}, +{ + "pk": 11, + "model": "auth.permission", + "fields": { + "codename": "change_contenttype", + "name": "Can change content type", + "content_type": 4 + } +}, +{ + "pk": 12, + "model": "auth.permission", + "fields": { + "codename": "delete_contenttype", + "name": "Can delete content type", + "content_type": 4 + } +}, +{ + "pk": 13, + "model": "auth.permission", + "fields": { + "codename": "add_session", + "name": "Can add session", + "content_type": 5 + } +}, +{ + "pk": 14, + "model": "auth.permission", + "fields": { + "codename": "change_session", + "name": "Can change session", + "content_type": 5 + } +}, +{ + "pk": 15, + "model": "auth.permission", + "fields": { + "codename": "delete_session", + "name": "Can delete session", + "content_type": 5 + } +}, +{ + "pk": 16, + "model": "auth.permission", + "fields": { + "codename": "add_site", + "name": "Can add site", + "content_type": 6 + } +}, +{ + "pk": 17, + "model": "auth.permission", + "fields": { + "codename": "change_site", + "name": "Can change site", + "content_type": 6 + } +}, +{ + "pk": 18, + "model": "auth.permission", + "fields": { + "codename": "delete_site", + "name": "Can delete site", + "content_type": 6 + } +}, +{ + "pk": 19, + "model": "auth.permission", + "fields": { + "codename": "add_logentry", + "name": "Can add log entry", + "content_type": 7 + } +}, +{ + "pk": 20, + "model": "auth.permission", + "fields": { + "codename": "change_logentry", + "name": "Can change log entry", + "content_type": 7 + } +}, +{ + "pk": 21, + "model": "auth.permission", + "fields": { + "codename": "delete_logentry", + "name": "Can delete log entry", + "content_type": 7 + } +}, +{ + "pk": 22, + "model": "auth.permission", + "fields": { + "codename": "add_migrationhistory", + "name": "Can add migration history", + "content_type": 8 + } +}, +{ + "pk": 23, + "model": "auth.permission", + "fields": { + "codename": "change_migrationhistory", + "name": "Can change migration history", + "content_type": 8 + } +}, +{ + "pk": 24, + "model": "auth.permission", + "fields": { + "codename": "delete_migrationhistory", + "name": "Can delete migration history", + "content_type": 8 + } +}, +{ + "pk": 25, + "model": "auth.permission", + "fields": { + "codename": "add_namedbaseresourceconfig", + "name": "Can add named base resource config", + "content_type": 9 + } +}, +{ + "pk": 26, + "model": "auth.permission", + "fields": { + "codename": "change_namedbaseresourceconfig", + "name": "Can change named base resource config", + "content_type": 9 + } +}, +{ + "pk": 27, + "model": "auth.permission", + "fields": { + "codename": "delete_namedbaseresourceconfig", + "name": "Can delete named base resource config", + "content_type": 9 + } +}, +{ + "pk": 28, + "model": "auth.permission", + "fields": { + "codename": "add_node", + "name": "Can add node", + "content_type": 10 + } +}, +{ + "pk": 29, + "model": "auth.permission", + "fields": { + "codename": "change_node", + "name": "Can change node", + "content_type": 10 + } +}, +{ + "pk": 30, + "model": "auth.permission", + "fields": { + "codename": "delete_node", + "name": "Can delete node", + "content_type": 10 + } +}, +{ + "pk": 31, + "model": "auth.permission", + "fields": { + "codename": "add_nodeactivity", + "name": "Can add node activity", + "content_type": 11 + } +}, +{ + "pk": 32, + "model": "auth.permission", + "fields": { + "codename": "change_nodeactivity", + "name": "Can change node activity", + "content_type": 11 + } +}, +{ + "pk": 33, + "model": "auth.permission", + "fields": { + "codename": "delete_nodeactivity", + "name": "Can delete node activity", + "content_type": 11 + } +}, +{ + "pk": 34, + "model": "auth.permission", + "fields": { + "codename": "add_lease", + "name": "Can add lease", + "content_type": 12 + } +}, +{ + "pk": 35, + "model": "auth.permission", + "fields": { + "codename": "change_lease", + "name": "Can change lease", + "content_type": 12 + } +}, +{ + "pk": 36, + "model": "auth.permission", + "fields": { + "codename": "delete_lease", + "name": "Can delete lease", + "content_type": 12 + } +}, +{ + "pk": 37, + "model": "auth.permission", + "fields": { + "codename": "add_instancetemplate", + "name": "Can add template", + "content_type": 13 + } +}, +{ + "pk": 38, + "model": "auth.permission", + "fields": { + "codename": "change_instancetemplate", + "name": "Can change template", + "content_type": 13 + } +}, +{ + "pk": 39, + "model": "auth.permission", + "fields": { + "codename": "delete_instancetemplate", + "name": "Can delete template", + "content_type": 13 + } +}, +{ + "pk": 40, + "model": "auth.permission", + "fields": { + "codename": "add_interfacetemplate", + "name": "Can add interface template", + "content_type": 14 + } +}, +{ + "pk": 41, + "model": "auth.permission", + "fields": { + "codename": "change_interfacetemplate", + "name": "Can change interface template", + "content_type": 14 + } +}, +{ + "pk": 42, + "model": "auth.permission", + "fields": { + "codename": "delete_interfacetemplate", + "name": "Can delete interface template", + "content_type": 14 + } +}, +{ + "pk": 43, + "model": "auth.permission", + "fields": { + "codename": "add_instance", + "name": "Can add instance", + "content_type": 15 + } +}, +{ + "pk": 44, + "model": "auth.permission", + "fields": { + "codename": "change_instance", + "name": "Can change instance", + "content_type": 15 + } +}, +{ + "pk": 45, + "model": "auth.permission", + "fields": { + "codename": "delete_instance", + "name": "Can delete instance", + "content_type": 15 + } +}, +{ + "pk": 46, + "model": "auth.permission", + "fields": { + "codename": "add_instanceactivity", + "name": "Can add instance activity", + "content_type": 16 + } +}, +{ + "pk": 47, + "model": "auth.permission", + "fields": { + "codename": "change_instanceactivity", + "name": "Can change instance activity", + "content_type": 16 + } +}, +{ + "pk": 48, + "model": "auth.permission", + "fields": { + "codename": "delete_instanceactivity", + "name": "Can delete instance activity", + "content_type": 16 + } +}, +{ + "pk": 49, + "model": "auth.permission", + "fields": { + "codename": "add_interface", + "name": "Can add interface", + "content_type": 17 + } +}, +{ + "pk": 50, + "model": "auth.permission", + "fields": { + "codename": "change_interface", + "name": "Can change interface", + "content_type": 17 + } +}, +{ + "pk": 51, + "model": "auth.permission", + "fields": { + "codename": "delete_interface", + "name": "Can delete interface", + "content_type": 17 + } +}, +{ + "pk": 52, + "model": "auth.permission", + "fields": { + "codename": "add_datastore", + "name": "Can add datastore", + "content_type": 18 + } +}, +{ + "pk": 53, + "model": "auth.permission", + "fields": { + "codename": "change_datastore", + "name": "Can change datastore", + "content_type": 18 + } +}, +{ + "pk": 54, + "model": "auth.permission", + "fields": { + "codename": "delete_datastore", + "name": "Can delete datastore", + "content_type": 18 + } +}, +{ + "pk": 55, + "model": "auth.permission", + "fields": { + "codename": "add_disk", + "name": "Can add disk", + "content_type": 19 + } +}, +{ + "pk": 56, + "model": "auth.permission", + "fields": { + "codename": "change_disk", + "name": "Can change disk", + "content_type": 19 + } +}, +{ + "pk": 57, + "model": "auth.permission", + "fields": { + "codename": "delete_disk", + "name": "Can delete disk", + "content_type": 19 + } +}, +{ + "pk": 58, + "model": "auth.permission", + "fields": { + "codename": "add_rule", + "name": "Can add rule", + "content_type": 20 + } +}, +{ + "pk": 59, + "model": "auth.permission", + "fields": { + "codename": "change_rule", + "name": "Can change rule", + "content_type": 20 + } +}, +{ + "pk": 60, + "model": "auth.permission", + "fields": { + "codename": "delete_rule", + "name": "Can delete rule", + "content_type": 20 + } +}, +{ + "pk": 61, + "model": "auth.permission", + "fields": { + "codename": "add_vlan", + "name": "Can add vlan", + "content_type": 21 + } +}, +{ + "pk": 62, + "model": "auth.permission", + "fields": { + "codename": "change_vlan", + "name": "Can change vlan", + "content_type": 21 + } +}, +{ + "pk": 63, + "model": "auth.permission", + "fields": { + "codename": "delete_vlan", + "name": "Can delete vlan", + "content_type": 21 + } +}, +{ + "pk": 64, + "model": "auth.permission", + "fields": { + "codename": "add_vlangroup", + "name": "Can add vlan group", + "content_type": 22 + } +}, +{ + "pk": 65, + "model": "auth.permission", + "fields": { + "codename": "change_vlangroup", + "name": "Can change vlan group", + "content_type": 22 + } +}, +{ + "pk": 66, + "model": "auth.permission", + "fields": { + "codename": "delete_vlangroup", + "name": "Can delete vlan group", + "content_type": 22 + } +}, +{ + "pk": 67, + "model": "auth.permission", + "fields": { + "codename": "add_group", + "name": "Can add group", + "content_type": 23 + } +}, +{ + "pk": 68, + "model": "auth.permission", + "fields": { + "codename": "change_group", + "name": "Can change group", + "content_type": 23 + } +}, +{ + "pk": 69, + "model": "auth.permission", + "fields": { + "codename": "delete_group", + "name": "Can delete group", + "content_type": 23 + } +}, +{ + "pk": 70, + "model": "auth.permission", + "fields": { + "codename": "add_host", + "name": "Can add host", + "content_type": 24 + } +}, +{ + "pk": 71, + "model": "auth.permission", + "fields": { + "codename": "change_host", + "name": "Can change host", + "content_type": 24 + } +}, +{ + "pk": 72, + "model": "auth.permission", + "fields": { + "codename": "delete_host", + "name": "Can delete host", + "content_type": 24 + } +}, +{ + "pk": 73, + "model": "auth.permission", + "fields": { + "codename": "add_firewall", + "name": "Can add firewall", + "content_type": 25 + } +}, +{ + "pk": 74, + "model": "auth.permission", + "fields": { + "codename": "change_firewall", + "name": "Can change firewall", + "content_type": 25 + } +}, +{ + "pk": 75, + "model": "auth.permission", + "fields": { + "codename": "delete_firewall", + "name": "Can delete firewall", + "content_type": 25 + } +}, +{ + "pk": 76, + "model": "auth.permission", + "fields": { + "codename": "add_domain", + "name": "Can add domain", + "content_type": 26 + } +}, +{ + "pk": 77, + "model": "auth.permission", + "fields": { + "codename": "change_domain", + "name": "Can change domain", + "content_type": 26 + } +}, +{ + "pk": 78, + "model": "auth.permission", + "fields": { + "codename": "delete_domain", + "name": "Can delete domain", + "content_type": 26 + } +}, +{ + "pk": 79, + "model": "auth.permission", + "fields": { + "codename": "add_record", + "name": "Can add record", + "content_type": 27 + } +}, +{ + "pk": 80, + "model": "auth.permission", + "fields": { + "codename": "change_record", + "name": "Can change record", + "content_type": 27 + } +}, +{ + "pk": 81, + "model": "auth.permission", + "fields": { + "codename": "delete_record", + "name": "Can delete record", + "content_type": 27 + } +}, +{ + "pk": 82, + "model": "auth.permission", + "fields": { + "codename": "add_blacklist", + "name": "Can add blacklist", + "content_type": 28 + } +}, +{ + "pk": 83, + "model": "auth.permission", + "fields": { + "codename": "change_blacklist", + "name": "Can change blacklist", + "content_type": 28 + } +}, +{ + "pk": 84, + "model": "auth.permission", + "fields": { + "codename": "delete_blacklist", + "name": "Can delete blacklist", + "content_type": 28 + } +}, +{ + "pk": 85, + "model": "auth.permission", + "fields": { + "codename": "add_taskmeta", + "name": "Can add task state", + "content_type": 29 + } +}, +{ + "pk": 86, + "model": "auth.permission", + "fields": { + "codename": "change_taskmeta", + "name": "Can change task state", + "content_type": 29 + } +}, +{ + "pk": 87, + "model": "auth.permission", + "fields": { + "codename": "delete_taskmeta", + "name": "Can delete task state", + "content_type": 29 + } +}, +{ + "pk": 88, + "model": "auth.permission", + "fields": { + "codename": "add_tasksetmeta", + "name": "Can add saved group result", + "content_type": 30 + } +}, +{ + "pk": 89, + "model": "auth.permission", + "fields": { + "codename": "change_tasksetmeta", + "name": "Can change saved group result", + "content_type": 30 + } +}, +{ + "pk": 90, + "model": "auth.permission", + "fields": { + "codename": "delete_tasksetmeta", + "name": "Can delete saved group result", + "content_type": 30 + } +}, +{ + "pk": 91, + "model": "auth.permission", + "fields": { + "codename": "add_intervalschedule", + "name": "Can add interval", + "content_type": 31 + } +}, +{ + "pk": 92, + "model": "auth.permission", + "fields": { + "codename": "change_intervalschedule", + "name": "Can change interval", + "content_type": 31 + } +}, +{ + "pk": 93, + "model": "auth.permission", + "fields": { + "codename": "delete_intervalschedule", + "name": "Can delete interval", + "content_type": 31 + } +}, +{ + "pk": 94, + "model": "auth.permission", + "fields": { + "codename": "add_crontabschedule", + "name": "Can add crontab", + "content_type": 32 + } +}, +{ + "pk": 95, + "model": "auth.permission", + "fields": { + "codename": "change_crontabschedule", + "name": "Can change crontab", + "content_type": 32 + } +}, +{ + "pk": 96, + "model": "auth.permission", + "fields": { + "codename": "delete_crontabschedule", + "name": "Can delete crontab", + "content_type": 32 + } +}, +{ + "pk": 97, + "model": "auth.permission", + "fields": { + "codename": "add_periodictasks", + "name": "Can add periodic tasks", + "content_type": 33 + } +}, +{ + "pk": 98, + "model": "auth.permission", + "fields": { + "codename": "change_periodictasks", + "name": "Can change periodic tasks", + "content_type": 33 + } +}, +{ + "pk": 99, + "model": "auth.permission", + "fields": { + "codename": "delete_periodictasks", + "name": "Can delete periodic tasks", + "content_type": 33 + } +}, +{ + "pk": 100, + "model": "auth.permission", + "fields": { + "codename": "add_periodictask", + "name": "Can add periodic task", + "content_type": 34 + } +}, +{ + "pk": 101, + "model": "auth.permission", + "fields": { + "codename": "change_periodictask", + "name": "Can change periodic task", + "content_type": 34 + } +}, +{ + "pk": 102, + "model": "auth.permission", + "fields": { + "codename": "delete_periodictask", + "name": "Can delete periodic task", + "content_type": 34 + } +}, +{ + "pk": 103, + "model": "auth.permission", + "fields": { + "codename": "add_workerstate", + "name": "Can add worker", + "content_type": 35 + } +}, +{ + "pk": 104, + "model": "auth.permission", + "fields": { + "codename": "change_workerstate", + "name": "Can change worker", + "content_type": 35 + } +}, +{ + "pk": 105, + "model": "auth.permission", + "fields": { + "codename": "delete_workerstate", + "name": "Can delete worker", + "content_type": 35 + } +}, +{ + "pk": 106, + "model": "auth.permission", + "fields": { + "codename": "add_taskstate", + "name": "Can add task", + "content_type": 36 + } +}, +{ + "pk": 107, + "model": "auth.permission", + "fields": { + "codename": "change_taskstate", + "name": "Can change task", + "content_type": 36 + } +}, +{ + "pk": 108, + "model": "auth.permission", + "fields": { + "codename": "delete_taskstate", + "name": "Can delete task", + "content_type": 36 + } +}, +{ + "pk": 109, + "model": "auth.permission", + "fields": { + "codename": "add_userobjectpermission", + "name": "Can add user object permission", + "content_type": 37 + } +}, +{ + "pk": 110, + "model": "auth.permission", + "fields": { + "codename": "change_userobjectpermission", + "name": "Can change user object permission", + "content_type": 37 + } +}, +{ + "pk": 111, + "model": "auth.permission", + "fields": { + "codename": "delete_userobjectpermission", + "name": "Can delete user object permission", + "content_type": 37 + } +}, +{ + "pk": 112, + "model": "auth.permission", + "fields": { + "codename": "add_groupobjectpermission", + "name": "Can add group object permission", + "content_type": 38 + } +}, +{ + "pk": 113, + "model": "auth.permission", + "fields": { + "codename": "change_groupobjectpermission", + "name": "Can change group object permission", + "content_type": 38 + } +}, +{ + "pk": 114, + "model": "auth.permission", + "fields": { + "codename": "delete_groupobjectpermission", + "name": "Can delete group object permission", + "content_type": 38 + } +}, +{ + "pk": 115, + "model": "auth.permission", + "fields": { + "codename": "own_instance", + "name": "owner", + "content_type": 15 + } +}, +{ + "pk": 116, + "model": "auth.permission", + "fields": { + "codename": "operate_instance", + "name": "operator", + "content_type": 15 + } +}, +{ + "pk": 117, + "model": "auth.permission", + "fields": { + "codename": "use_instance", + "name": "user", + "content_type": 15 + } +}, +{ + "pk": 118, + "model": "auth.permission", + "fields": { + "codename": "add_diskactivity", + "name": "Can add disk activity", + "content_type": 39 + } +}, +{ + "pk": 119, + "model": "auth.permission", + "fields": { + "codename": "change_diskactivity", + "name": "Can change disk activity", + "content_type": 39 + } +}, +{ + "pk": 120, + "model": "auth.permission", + "fields": { + "codename": "delete_diskactivity", + "name": "Can delete disk activity", + "content_type": 39 + } +}, +{ + "pk": 121, + "model": "auth.permission", + "fields": { + "codename": "add_level", + "name": "Can add level", + "content_type": 40 + } +}, +{ + "pk": 122, + "model": "auth.permission", + "fields": { + "codename": "change_level", + "name": "Can change level", + "content_type": 40 + } +}, +{ + "pk": 123, + "model": "auth.permission", + "fields": { + "codename": "delete_level", + "name": "Can delete level", + "content_type": 40 + } +}, +{ + "pk": 127, + "model": "auth.permission", + "fields": { + "codename": "add_objectlevel", + "name": "Can add object level", + "content_type": 42 + } +}, +{ + "pk": 128, + "model": "auth.permission", + "fields": { + "codename": "change_objectlevel", + "name": "Can change object level", + "content_type": 42 + } +}, +{ + "pk": 129, + "model": "auth.permission", + "fields": { + "codename": "delete_objectlevel", + "name": "Can delete object level", + "content_type": 42 + } +}, +{ + "pk": 130, + "model": "auth.permission", + "fields": { + "codename": "add_switchport", + "name": "Can add switch port", + "content_type": 43 + } +}, +{ + "pk": 131, + "model": "auth.permission", + "fields": { + "codename": "change_switchport", + "name": "Can change switch port", + "content_type": 43 + } +}, +{ + "pk": 132, + "model": "auth.permission", + "fields": { + "codename": "delete_switchport", + "name": "Can delete switch port", + "content_type": 43 + } +}, +{ + "pk": 133, + "model": "auth.permission", + "fields": { + "codename": "add_ethernetdevice", + "name": "Can add ethernet device", + "content_type": 44 + } +}, +{ + "pk": 134, + "model": "auth.permission", + "fields": { + "codename": "change_ethernetdevice", + "name": "Can change ethernet device", + "content_type": 44 + } +}, +{ + "pk": 135, + "model": "auth.permission", + "fields": { + "codename": "delete_ethernetdevice", + "name": "Can delete ethernet device", + "content_type": 44 + } +}, +{ + "pk": 1, + "model": "auth.group", + "fields": { + "name": "csopi", + "permissions": [] + } +}, +{ + "pk": 2, + "model": "auth.group", + "fields": { + "name": "akkorszia", + "permissions": [] + } +}, +{ + "pk": 3, + "model": "auth.group", + "fields": { + "name": "akkountsoherek", + "permissions": [] + } +}, +{ + "pk": 4, + "model": "auth.group", + "fields": { + "name": "helog", + "permissions": [] + } +}, +{ + "pk": -1, + "model": "auth.user", + "fields": { + "username": "AnonymousUser", + "first_name": "", + "last_name": "", + "is_active": true, + "is_superuser": false, + "is_staff": false, + "last_login": "2013-09-30T12:37:38.484Z", + "groups": [], + "user_permissions": [], + "password": "", + "email": "", + "date_joined": "2013-09-30T12:37:38.484Z" + } +}, +{ + "pk": 1, + "model": "auth.user", + "fields": { + "username": "test", + "first_name": "", + "last_name": "", + "is_active": true, + "is_superuser": true, + "is_staff": true, + "last_login": "2013-11-09T03:40:23.157Z", + "groups": [], + "user_permissions": [ + 115 + ], + "password": "pbkdf2_sha256$10000$KIoeMs78MiOj$PnVXn3YJMehbOciBO32CMzqL0ZnQrzrdb7+b5dE13os=", + "email": "test@example.org", + "date_joined": "2013-09-04T15:29:49.914Z" + } +}, +{ + "pk": 3, + "model": "auth.user", + "fields": { + "username": "proba", + "first_name": "", + "last_name": "", + "is_active": true, + "is_superuser": false, + "is_staff": false, + "last_login": "2013-10-14T06:59:48Z", + "groups": [ + 1 + ], + "user_permissions": [], + "password": "", + "email": "", + "date_joined": "2013-10-14T06:59:48Z" + } +}, +{ + "pk": 4, + "model": "auth.user", + "fields": { + "username": "helo", + "first_name": "", + "last_name": "", + "is_active": true, + "is_superuser": false, + "is_staff": false, + "last_login": "2013-11-09T01:11:41.534Z", + "groups": [ + 4 + ], + "user_permissions": [], + "password": "", + "email": "", + "date_joined": "2013-11-09T01:11:41.534Z" + } +}, +{ + "pk": 1, + "model": "vm.instance", + "fields": { + "destoryed": null, + "disks": [ + 1 + ], + "boot_menu": false, + "owner": 1, + "time_of_delete": null, + "max_ram_size": 200, + "pw": "ads", + "time_of_suspend": null, + "ram_size": 200, + "priority": 4, + "active_since": null, + "state": "NOSTATE", + "template": null, + "access_method": "nx", + "lease": 1, + "node": null, + "description": "", + "arch": "x86_64", + "name": "vanneve", + "created": "2013-09-16T09:05:59.991Z", + "raw_data": "", + "vnc_port": 1234, + "num_cores": 2, + "modified": "2013-10-14T07:27:38.192Z" + } +} +] diff --git a/circle/dashboard/templates/base.html b/circle/dashboard/templates/base.html new file mode 100644 index 0000000..d95e4ba --- /dev/null +++ b/circle/dashboard/templates/base.html @@ -0,0 +1,43 @@ +{% 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>© Company 2013</p> + </footer> + </div> <!-- /container --> + + </body> +{% block extra_js %} +{% endblock %} +</html> diff --git a/circle/dashboard/templates/dashboard/vm-detail-access.html b/circle/dashboard/templates/dashboard/vm-detail-access.html index 9fcabfe..6b6e636 100644 --- a/circle/dashboard/templates/dashboard/vm-detail-access.html +++ b/circle/dashboard/templates/dashboard/vm-detail-access.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> diff --git a/circle/dashboard/tests.py b/circle/dashboard/tests.py deleted file mode 100644 index 501deb7..0000000 --- a/circle/dashboard/tests.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -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) diff --git a/circle/dashboard/tests/__init__.py b/circle/dashboard/tests/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/circle/dashboard/tests/__init__.py diff --git a/circle/dashboard/tests/test_vm.py b/circle/dashboard/tests/test_vm.py new file mode 100644 index 0000000..5c5e92a --- /dev/null +++ b/circle/dashboard/tests/test_vm.py @@ -0,0 +1,26 @@ +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) diff --git a/circle/dashboard/urls.py b/circle/dashboard/urls.py index 9a1b0c1..2cfd9de 100644 --- a/circle/dashboard/urls.py +++ b/circle/dashboard/urls.py @@ -1,6 +1,9 @@ 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'), diff --git a/circle/dashboard/views.py b/circle/dashboard/views.py index daf43dc..419892e 100644 --- a/circle/dashboard/views.py +++ b/circle/dashboard/views.py @@ -1,3 +1,17 @@ +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): + name = request.POST['perm-new-name'] + value = request.POST['perm-new'] + if not name: + return + try: + entity = User.objects.get(username=name) + except User.DoesNotExist: + entity = None + try: + entity = Group.objects.get(name=name) + except Group.DoesNotExist: + warning(request, _('User or group "%s" not found.') % name) + return + + instance.set_level(entity, value) + logger.info("Set %s's new acl level for %s to %s by %s.", + unicode(entity), unicode(instance), + value, unicode(request.user)) + + class TemplateDetail(DetailView): model = InstanceTemplate diff --git a/circle/firewall/migrations/0002_auto__add_field_rule_owner__chg_field_host_mac.py b/circle/firewall/migrations/0002_auto__add_field_rule_owner__chg_field_host_mac.py index 2eeb23a..6c022a7 100644 --- a/circle/firewall/migrations/0002_auto__add_field_rule_owner__chg_field_host_mac.py +++ b/circle/firewall/migrations/0002_auto__add_field_rule_owner__chg_field_host_mac.py @@ -10,7 +10,7 @@ class Migration(SchemaMigration): def forwards(self, orm): # Adding field 'Rule.owner' db.add_column('firewall_rule', 'owner', - self.gf('django.db.models.fields.related.ForeignKey')(default=None, to=orm['auth.User']), + self.gf('django.db.models.fields.related.ForeignKey')(default=1, to=orm['auth.User']), keep_default=False) @@ -120,4 +120,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['firewall'] \ No newline at end of file + complete_apps = ['firewall'] diff --git a/circle/firewall/migrations/0005_auto__del_field_rule_vlan__add_field_host_vlan.py b/circle/firewall/migrations/0005_auto__del_field_rule_vlan__add_field_host_vlan.py index b38d6c7..a788879 100644 --- a/circle/firewall/migrations/0005_auto__del_field_rule_vlan__add_field_host_vlan.py +++ b/circle/firewall/migrations/0005_auto__del_field_rule_vlan__add_field_host_vlan.py @@ -21,7 +21,7 @@ class Migration(SchemaMigration): # Adding field 'Host.vlan' db.add_column('firewall_host', 'vlan', - self.gf('django.db.models.fields.related.ForeignKey')(default=None, to=orm['firewall.Vlan']), + self.gf('django.db.models.fields.related.ForeignKey')(default=1, to=orm['firewall.Vlan']), keep_default=False) # Removing M2M table for field vlan on 'Host' @@ -142,4 +142,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['firewall'] \ No newline at end of file + complete_apps = ['firewall'] diff --git a/circle/firewall/migrations/0008_auto__add_field_rule_nat__add_field_rule_nat_dport.py b/circle/firewall/migrations/0008_auto__add_field_rule_nat__add_field_rule_nat_dport.py index 246c722..e4c1cfc 100644 --- a/circle/firewall/migrations/0008_auto__add_field_rule_nat__add_field_rule_nat_dport.py +++ b/circle/firewall/migrations/0008_auto__add_field_rule_nat__add_field_rule_nat_dport.py @@ -15,7 +15,7 @@ class Migration(SchemaMigration): # Adding field 'Rule.nat_dport' db.add_column('firewall_rule', 'nat_dport', - self.gf('django.db.models.fields.IntegerField')(default=None), + self.gf('django.db.models.fields.IntegerField')(default=1), keep_default=False) @@ -126,4 +126,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['firewall'] \ No newline at end of file + complete_apps = ['firewall'] diff --git a/circle/firewall/migrations/0022_auto__add_vlangroup__add_field_setting_description__add_field_group_de.py b/circle/firewall/migrations/0022_auto__add_vlangroup__add_field_setting_description__add_field_group_de.py index 8ae2c24..3c89cd9 100644 --- a/circle/firewall/migrations/0022_auto__add_vlangroup__add_field_setting_description__add_field_group_de.py +++ b/circle/firewall/migrations/0022_auto__add_vlangroup__add_field_setting_description__add_field_group_de.py @@ -58,7 +58,7 @@ class Migration(SchemaMigration): # Adding field 'Rule.foreign_network' db.add_column('firewall_rule', 'foreign_network', - self.gf('django.db.models.fields.related.ForeignKey')(default=None, related_name='ForeignRules', to=orm['firewall.VlanGroup']), + self.gf('django.db.models.fields.related.ForeignKey')(default=1, related_name='ForeignRules', to=orm['firewall.VlanGroup']), keep_default=False) # Adding field 'Rule.vlan' @@ -295,4 +295,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['firewall'] \ No newline at end of file + complete_apps = ['firewall'] diff --git a/circle/firewall/migrations/0029_auto__add_field_vlan_domain.py b/circle/firewall/migrations/0029_auto__add_field_vlan_domain.py index cfd7fcb..6e34218 100644 --- a/circle/firewall/migrations/0029_auto__add_field_vlan_domain.py +++ b/circle/firewall/migrations/0029_auto__add_field_vlan_domain.py @@ -10,7 +10,7 @@ class Migration(SchemaMigration): def forwards(self, orm): # Adding field 'Vlan.domain' db.add_column('firewall_vlan', 'domain', - self.gf('django.db.models.fields.related.ForeignKey')(default=None, to=orm['firewall.Domain']), + self.gf('django.db.models.fields.related.ForeignKey')(default=1, to=orm['firewall.Domain']), keep_default=False) @@ -187,4 +187,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['firewall'] \ No newline at end of file + complete_apps = ['firewall'] diff --git a/circle/storage/migrations/0003_auto__add_datastore__del_field_disk_path__add_field_disk_datastore.py b/circle/storage/migrations/0003_auto__add_datastore__del_field_disk_path__add_field_disk_datastore.py index 5aa40c9..3e48fb0 100644 --- a/circle/storage/migrations/0003_auto__add_datastore__del_field_disk_path__add_field_disk_datastore.py +++ b/circle/storage/migrations/0003_auto__add_datastore__del_field_disk_path__add_field_disk_datastore.py @@ -21,7 +21,7 @@ class Migration(SchemaMigration): # Adding field 'Disk.datastore' db.add_column('storage_disk', 'datastore', - self.gf('django.db.models.fields.related.ForeignKey')(default=None, to=orm['storage.DataStore']), + self.gf('django.db.models.fields.related.ForeignKey')(default=1, to=orm['storage.DataStore']), keep_default=False) @@ -56,4 +56,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['storage'] \ No newline at end of file + complete_apps = ['storage'] diff --git a/circle/templates/404.html b/circle/templates/404.html index 61d6d9f..6f57b74 100644 --- a/circle/templates/404.html +++ b/circle/templates/404.html @@ -1,5 +1,5 @@ -{% load i18n %} {% extends "base.html" %} +{% load i18n %} {% block title %}{% trans "Page not found" %}{% endblock %} diff --git a/circle/templates/500.html b/circle/templates/500.html index 31fb0c3..93a17c1 100644 --- a/circle/templates/500.html +++ b/circle/templates/500.html @@ -1,5 +1,5 @@ -{% load i18n %} {% extends "base.html" %} +{% load i18n %} {% block title %}HTTP 500{% endblock %} diff --git a/circle/vm/models.py b/circle/vm/models.py index 46fac7f..8058eb2 100644 --- a/circle/vm/models.py +++ b/circle/vm/models.py @@ -20,8 +20,7 @@ from .tasks import local_tasks, vm_tasks, net_tasks from firewall.models import Vlan, Host from storage.models import Disk from common.models import ActivityModel, activitycontextimpl -from django.core import signing - +from acl.models import AclBase logger = logging.getLogger(__name__) pwgen = User.objects.make_random_password @@ -271,7 +270,7 @@ class InterfaceTemplate(Model): verbose_name_plural = _('interface templates') -class Instance(VirtualMachineDescModel, TimeStampedModel): +class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel): """Virtual machine instance. @@ -297,6 +296,11 @@ class Instance(VirtualMachineDescModel, TimeStampedModel): ('SHUTOFF', _('shutoff')), ('CRASHED', _('crashed')), ('PMSUSPENDED', _('pmsuspended'))] # libvirt domain states + ACL_LEVELS = ( + ('user', _('user')), # see all details + ('operator', _('operator')), # console, networking, change state + ('owner', _('owner')), # superuser, can delete, delegate perms + ) name = CharField(blank=True, max_length=100, verbose_name=_('name'), help_text=_("Human readable name of instance.")) description = TextField(blank=True, verbose_name=_('description')) @@ -337,7 +341,6 @@ class Instance(VirtualMachineDescModel, TimeStampedModel): class Meta: ordering = ['pk', ] - permissions = () verbose_name = _('instance') verbose_name_plural = _('instances') diff --git a/circle/vm/tests/test_models.py b/circle/vm/tests/test_models.py index ca14d68..a05d595 100644 --- a/circle/vm/tests/test_models.py +++ b/circle/vm/tests/test_models.py @@ -1,8 +1,9 @@ from django.test import TestCase -from .models import Template +from ..models import InstanceTemplate class TemplateTestCase(TestCase): def test_template_creation(self): - template = Template(name='My first template', - access_method='ssh', ) # TODO add images & net + template = InstanceTemplate(name='My first template', + access_method='ssh', ) + # TODO add images & net