Commit 3ec83939 by Belákovics Ádám

Merge branch 'instance_auth' into 'DEV'

Instance auth

See merge request !17
parents c0d7f992 befe5fa1
Pipeline #862 passed with stage
in 1 minute 22 seconds
...@@ -17,6 +17,7 @@ django-cors-headers = "*" ...@@ -17,6 +17,7 @@ django-cors-headers = "*"
openstacksdk = "*" openstacksdk = "*"
python-novaclient = "*" python-novaclient = "*"
keystoneauth1 = "*" keystoneauth1 = "*"
django-guardian = "*"
djoser = "*" djoser = "*"
[requires] [requires]
......
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "8cb2efb1cfa79de96297c90953faf757011ca1f1ff4784ac05226a249001e1f5" "sha256": "dd7bfbb33d07cbcc96d1f3b1f838538dba41f3eb0bf5279381714b4b9abf90d7"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
...@@ -135,6 +135,14 @@ ...@@ -135,6 +135,14 @@
"index": "pypi", "index": "pypi",
"version": "==3.0.2" "version": "==3.0.2"
}, },
"django-guardian": {
"hashes": [
"sha256:965d3a1e20fb3639e0ab16b0e768611f694c02d762916f80d9f3f7520c16aa7b",
"sha256:e8c4556c4e145028a5dcfd7b2611d52e1ac104af562017ce17c3f67e47a62693"
],
"index": "pypi",
"version": "==2.0.0"
},
"django-templated-mail": { "django-templated-mail": {
"hashes": [ "hashes": [
"sha256:8db807effebb42a532622e2d142dfd453dafcd0d7794c4c3332acb90656315f9", "sha256:8db807effebb42a532622e2d142dfd453dafcd0d7794c4c3332acb90656315f9",
......
from django.contrib.auth.models import Permission
from django.contrib import admin
admin.site.register(Permission)
from django.apps import AppConfig
class AuthorizationConfig(AppConfig):
name = 'authorization'
from guardian.shortcuts import get_objects_for_user
import logging
logger = logging.getLogger(__name__)
class AuthorizationMixin():
authorization = {}
def get_objects_with_perms(self, user, method, instance):
auth_params = self.authorization[method]
if auth_params:
return get_objects_for_user(user, auth_params["filter"], instance)
else:
logger.error(f"Invalid method for authorization: {method}")
return False
def has_perms_for_object(self, user, method, instance):
auth_params = self.authorization[method]
if auth_params:
for perm in auth_params["object"]:
if not user.has_perm(perm, instance):
return False
return True
else:
logger.error(f"Invalid method for authorization: {method}")
return False
def has_perms_for_model(self, user, method):
auth_params = self.authorization[method]
if auth_params:
for perm in auth_params["model"]:
if not user.has_perm(perm):
return False
return True
else:
logger.error(f"Invalid method for authorization: {method}")
return False
...@@ -2,6 +2,13 @@ from django.contrib import admin ...@@ -2,6 +2,13 @@ from django.contrib import admin
from instance.models import Instance, Flavor, Lease from instance.models import Instance, Flavor, Lease
admin.site.register(Instance) from guardian.admin import GuardedModelAdmin
class InstanceAdmin(GuardedModelAdmin):
pass
admin.site.register(Instance, InstanceAdmin)
admin.site.register(Flavor) admin.site.register(Flavor)
admin.site.register(Lease) admin.site.register(Lease)
# Generated by Django 2.2.4 on 2019-08-08 11:37
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('instance', '0010_instance_template'),
]
operations = [
migrations.AlterModelOptions(
name='instance',
options={'permissions': (('create_instance', 'Can create a new VM.'), ('use_instance', 'Can access the VM connection info.'), ('operate_instance', 'Can use basic lifecycle methods of the VM.'), ('administer_instance', 'Can delete VM.'), ('access_console', 'Can access the graphical console of a VM.'), ('change_resources', 'Can change resources of a VM.'), ('manage_access', 'Can manage access rights for the VM.'), ('config_ports', 'Can configure port forwards.'))},
),
]
# Generated by Django 2.2.4 on 2019-08-29 07:54
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('instance', '0011_auto_20190808_1137'),
]
operations = [
migrations.AlterModelOptions(
name='instance',
options={'default_permissions': (), 'permissions': (('create_instance', 'Can create a new VM.'), ('use_instance', 'Can access the VM connection info.'), ('operate_instance', 'Can use basic lifecycle methods of the VM.'), ('administer_instance', 'Can delete VM.'), ('access_console', 'Can access the graphical console of a VM.'), ('change_resources', 'Can change resources of a VM.'), ('manage_access', 'Can manage access rights for the VM.'), ('config_ports', 'Can configure port forwards.'))},
),
]
# Generated by Django 2.2.4 on 2019-08-30 12:27
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('instance', '0012_auto_20190829_0754'),
]
operations = [
migrations.AlterModelOptions(
name='instance',
options={'default_permissions': (), 'permissions': (('create_instance', 'Can create a new VM.'), ('create_template_from_instance', 'Can create template from instance.'), ('use_instance', 'Can access the VM connection info.'), ('operate_instance', 'Can use basic lifecycle methods of the VM.'), ('administer_instance', 'Can delete VM.'), ('access_console', 'Can access the graphical console of a VM.'), ('change_resources', 'Can change resources of a VM.'), ('manage_access', 'Can manage access rights for the VM.'), ('config_ports', 'Can configure port forwards.'))},
),
]
...@@ -5,8 +5,8 @@ from django.utils import timezone ...@@ -5,8 +5,8 @@ from django.utils import timezone
from datetime import timedelta from datetime import timedelta
from image.models import Disk from image.models import Disk
from interface_openstack.implementation.vm.instance import ( from interface_openstack.implementation.vm.instance import (
OSVirtualMachineManager OSVirtualMachineManager
) )
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -45,8 +45,8 @@ class Flavor(models.Model): ...@@ -45,8 +45,8 @@ class Flavor(models.Model):
name = models.CharField(blank=True, max_length=100) name = models.CharField(blank=True, max_length=100)
description = models.CharField(blank=True, max_length=200) description = models.CharField(blank=True, max_length=200)
remote_id = models.CharField( remote_id = models.CharField(
max_length=100, help_text="ID of the instance on the backend" max_length=100, help_text="ID of the instance on the backend"
) )
ram = models.IntegerField(blank=True, null=True) ram = models.IntegerField(blank=True, null=True)
vcpu = models.IntegerField(blank=True, null=True) vcpu = models.IntegerField(blank=True, null=True)
initial_disk = models.IntegerField(blank=True, null=True) initial_disk = models.IntegerField(blank=True, null=True)
...@@ -82,6 +82,20 @@ class Instance(models.Model): ...@@ -82,6 +82,20 @@ class Instance(models.Model):
""" """
from template.models import ImageTemplate from template.models import ImageTemplate
class Meta:
default_permissions = ()
permissions = (
('create_instance', 'Can create a new VM.'),
('create_template_from_instance', 'Can create template from instance.'),
('use_instance', 'Can access the VM connection info.'),
('operate_instance', 'Can use basic lifecycle methods of the VM.'),
('administer_instance', 'Can delete VM.'),
('access_console', 'Can access the graphical console of a VM.'),
('change_resources', 'Can change resources of a VM.'),
('manage_access', 'Can manage access rights for the VM.'),
('config_ports', 'Can configure port forwards.'),
)
name = models.CharField(max_length=100, name = models.CharField(max_length=100,
help_text="Human readable name of instance") help_text="Human readable name of instance")
...@@ -167,10 +181,20 @@ class Instance(models.Model): ...@@ -167,10 +181,20 @@ class Instance(models.Model):
lease = self.lease lease = self.lease
return ( return (
timezone.now() + timedelta( timezone.now() + timedelta(
seconds=lease.suspend_interval_in_sec), seconds=lease.suspend_interval_in_sec),
timezone.now() + timedelta( timezone.now() + timedelta(
seconds=lease.delete_interval_in_sec) seconds=lease.delete_interval_in_sec)
) )
def renew(self, lease=None):
"""Renew virtual machine, if a new lease is provided it changes it as well.
"""
if lease is None:
lease = self.lease
else:
self.lease = lease
self.time_of_suspend, self.time_of_delete = self.get_renew_times(lease)
self.save()
def delete(self): def delete(self):
try: try:
...@@ -192,5 +216,13 @@ class Instance(models.Model): ...@@ -192,5 +216,13 @@ class Instance(models.Model):
@classmethod @classmethod
def generate_password(self): def generate_password(self):
return User.objects.make_random_password( return User.objects.make_random_password(
allowed_chars='abcdefghijklmnopqrstuvwx' allowed_chars='abcdefghijklmnopqrstuvwx'
'ABCDEFGHIJKLMNOPQRSTUVWX123456789') 'ABCDEFGHIJKLMNOPQRSTUVWX123456789')
def change_name(self, new_name):
self.name = new_name
self.save()
def change_description(self, new_description):
self.description = new_description
self.save()
...@@ -9,9 +9,46 @@ from template.serializers import InstanceFromTemplateSerializer ...@@ -9,9 +9,46 @@ from template.serializers import InstanceFromTemplateSerializer
from instance.models import Instance, Flavor, Lease from instance.models import Instance, Flavor, Lease
from template.models import ImageTemplate from template.models import ImageTemplate
from template.serializers import ImageTemplateModelSerializer from template.serializers import ImageTemplateModelSerializer
from authorization.mixins import AuthorizationMixin
class InstanceViewSet(ViewSet):
authorization = {
"list": {"filter": ["use_instance"]},
"create": {"model": ["instance.create_instance"]},
"retrieve": {"object": ["use_instance"]},
"update": {"object": ["use_instance"]},
"destroy": {"object": ["administer_instance"]},
"template": {"model": ["create_template_from_instance"],
"object": ["use_instance"]},
"start": {"object": ["operate_instance"]},
"stop": {"object": ["operate_instance"]},
"suspend": {"object": ["operate_instance"]},
"wake_up": {"object": ["operate_instance"]},
"reset": {"object": ["operate_instance"]},
"reboot": {"object": ["operate_instance"]},
}
update_actions = [
"change_name",
"change_description",
"renew",
"change_lease",
"change_flavor",
"attach_disk",
"resize_disk",
"add_permission",
"remove_permission",
"open_port",
"close_port",
"add_network",
"remove_network",
"new_password",
]
class InstanceViewSet(AuthorizationMixin, ViewSet):
authorization = authorization
def get_object(self, pk): def get_object(self, pk):
try: try:
...@@ -20,10 +57,14 @@ class InstanceViewSet(ViewSet): ...@@ -20,10 +57,14 @@ class InstanceViewSet(ViewSet):
raise Http404 raise Http404
def list(self, request): def list(self, request):
instances = Instance.objects.all() instances = self.get_objects_with_perms(request.user, "list", Instance)
return Response(InstanceSerializer(instances, many=True).data) return Response(InstanceSerializer(instances, many=True).data)
def create(self, request): def create(self, request):
if not self.has_perms_for_model(request.user, 'create'):
return Response({"error": "No permission to create Virtual Machine."},
status=status.HTTP_401_UNAUTHORIZED)
data = request.data data = request.data
template = ImageTemplate.objects.get(pk=data["template"]) template = ImageTemplate.objects.get(pk=data["template"])
...@@ -50,6 +91,10 @@ class InstanceViewSet(ViewSet): ...@@ -50,6 +91,10 @@ class InstanceViewSet(ViewSet):
def retrieve(self, request, pk): def retrieve(self, request, pk):
instance = self.get_object(pk) instance = self.get_object(pk)
if not self.has_perms_for_object(request.user, 'retrieve', instance):
return Response({"error": "No permission to access the Virtual Machine."},
status=status.HTTP_401_UNAUTHORIZED)
instanceDict = InstanceSerializer(instance).data instanceDict = InstanceSerializer(instance).data
remoteInstance = instance.get_remote_instance() remoteInstance = instance.get_remote_instance()
remoteInstanceDict = remoteInstance.__dict__ remoteInstanceDict = remoteInstance.__dict__
...@@ -59,21 +104,67 @@ class InstanceViewSet(ViewSet): ...@@ -59,21 +104,67 @@ class InstanceViewSet(ViewSet):
return Response(merged_dict) return Response(merged_dict)
def update(self, request, pk, format=None): def update(self, request, pk, format=None):
instance = self.get_object(pk) if request.data["action"] in update_actions:
serializer = InstanceSerializer(instance, data=request.data) instance = self.get_object(pk)
if serializer.is_valid(): if not self.has_perms_for_object(request.user, 'update', instance):
serializer.save() return Response({"error": "No permission to access the Virtual Machine."},
return Response(serializer.data) status=status.HTTP_401_UNAUTHORIZED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) action = request.data["action"]
if action == "change_name":
instance.change_name(request.data["name"])
elif action == "change_description":
instance.change_description(request.data["description"])
elif action == "renew":
instance.renew()
elif action == "change_lease":
lease = Lease.objects.get(pk=request.data["lease"])
instance.renew(lease)
elif action == "change_flavor":
pass
elif action == "attach_disk":
pass
elif action == "resize_disk":
pass
elif action == "add_permission":
pass
elif action == "remove_permission":
pass
elif action == "open_port":
pass
elif action == "close_port":
pass
elif action == "add_network":
pass
elif action == "remove_network":
pass
elif action == "new_password":
pass
instanceDict = InstanceSerializer(instance).data
remoteInstance = instance.get_remote_instance()
remoteInstanceDict = remoteInstance.__dict__
merged_dict = {"db": instanceDict, "openstack": remoteInstanceDict}
return Response(merged_dict)
else:
return Response({"error": "Unknown update action."}, status=status.HTTP_400_BAD_REQUEST)
def destroy(self, request, pk, format=None): def destroy(self, request, pk, format=None):
instance = self.get_object(pk) instance = self.get_object(pk)
if not self.has_perms_for_object(request.user, 'destroy', instance):
return Response({"error": "No permission to destroy the Virtual Machine."},
status=status.HTTP_401_UNAUTHORIZED)
instance.delete() instance.delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
@action(detail=True, methods=["post"]) @action(detail=True, methods=["post"])
def template(self, request, pk): def template(self, request, pk):
instance = self.get_object(pk) instance = self.get_object(pk)
if not self.has_perms_for_model(request.user, 'template'):
return Response({"error": "No permission to create template from instance."},
status=status.HTTP_401_UNAUTHORIZED)
if not self.has_perms_for_object(request.user, 'template', instance):
return Response({"error": "No permission to access the Virtual Machine."},
status=status.HTTP_401_UNAUTHORIZED)
serializer = InstanceFromTemplateSerializer(data=request.data) serializer = InstanceFromTemplateSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
data = serializer.validated_data data = serializer.validated_data
...@@ -87,8 +178,10 @@ class InstanceViewSet(ViewSet): ...@@ -87,8 +178,10 @@ class InstanceViewSet(ViewSet):
@action(detail=True, methods=["POST"]) @action(detail=True, methods=["POST"])
def actions(self, request, pk): def actions(self, request, pk):
instance = self.get_object(pk) instance = self.get_object(pk)
if not self.has_perms_for_object(request.user, request.data["action"], instance):
return Response({"error": "No permission to use this action on the VM."},
status=status.HTTP_401_UNAUTHORIZED)
success = instance.execute_common_action(action=request.data["action"]) success = instance.execute_common_action(action=request.data["action"])
return Response(success) return Response(success)
......
...@@ -41,6 +41,7 @@ INSTALLED_APPS = [ ...@@ -41,6 +41,7 @@ INSTALLED_APPS = [
"djoser", "djoser",
"rest_framework_swagger", "rest_framework_swagger",
"corsheaders", "corsheaders",
"guardian",
"django_nose", "django_nose",
] ]
...@@ -49,6 +50,7 @@ LOCAL_APPS = [ ...@@ -49,6 +50,7 @@ LOCAL_APPS = [
"instance", "instance",
"storage", "storage",
"template", "template",
"authorization",
] ]
INSTALLED_APPS += LOCAL_APPS INSTALLED_APPS += LOCAL_APPS
...@@ -112,6 +114,7 @@ REST_FRAMEWORK = { ...@@ -112,6 +114,7 @@ REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': ( 'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.TokenAuthentication', 'rest_framework.authentication.TokenAuthentication',
), ),
'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema' # needed for swagger
} }
# Internationalization # Internationalization
...@@ -230,3 +233,10 @@ LOGGING = { ...@@ -230,3 +233,10 @@ LOGGING = {
for i in LOCAL_APPS: for i in LOCAL_APPS:
LOGGING['loggers'][i] = {'handlers': ['console'], 'level': 'DEBUG'} LOGGING['loggers'][i] = {'handlers': ['console'], 'level': 'DEBUG'}
# Configure django-guardian for the authorization
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend', # this is default
'guardian.backends.ObjectPermissionBackend',
)
...@@ -12,5 +12,8 @@ ADMIN_ENABLED = True ...@@ -12,5 +12,8 @@ ADMIN_ENABLED = True
ALLOWED_HOSTS = ['*'] ALLOWED_HOSTS = ['*']
AUTH_PASSWORD_VALIDATORS = []
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
NOSE_ARGS = LOCAL_APPS
\ No newline at end of file NOSE_ARGS = LOCAL_APPS
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment