diff --git a/ROADMAP.md b/ROADMAP.md index 0751b512..d7a14cde 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -65,19 +65,20 @@ WebVirtCloud kod tabanı; temel sanallaştırma orkestrasyonu için işlevsel bi ## 3. Önceliklendirilmiş Uygulama Planı (Aksiyon Listesi) -### Faz 1: P0 — Acil Güvenlik Düzeltmeleri (Hemen Uygulanacak) -1. **API Yetkilendirme & Secret İzolasyonu:** - - `ComputeViewSet`, `StorageViewSet`, `NetworksViewSet`, `InterfacesViewSet` sınıflarına `permission_classes = [permissions.IsAdminUser]` atanması. - - `ComputeSerializer` içindeki `password` alanına `write_only=True` verilmesi veya serializer çıktısından tamamen çıkarılması. - - `InstanceViewSet.list` üzerinde `request.user.is_superuser` ve `userinstance__user` kapsam denetiminin zorunlu kılınması. -2. **noVNC & Konsol Token Güvenliği:** - - `django.core.signing.TimestampSigner` ile imzalı, 60 saniye geçerli, kullanıcı-VM eşleştirmeli token üretimi. - - `novncd` ve `console/views.py` içinde imza ve süre doğrulamasının uygulanması. -3. **Varsayılan Hesap ve Arka Plan Daemon Sıkılaştırması:** - - `accounts/apps.py` içindeki otomatik `admin/admin` üretiminin kaldırılması. - - `gstfsd` servisinin devre dışı bırakılması veya Unix domain socket altına alınması. -4. **Appsettings & NWFilter İzin Sıkılaştırması:** - - `appsettings/views.py` ve `nwfilters/views.py` görünümlerine `@superuser_only` eklenmesi. +### Faz 1: P0 — Acil Güvenlik Düzeltmeleri (Tamamlandı ✅) +1. **API Yetkilendirme & Secret İzolasyonu:** [x] + - `ComputeViewSet`, `StorageViewSet`, `VolumeViewSet`, `NetworkViewSet`, `InterfaceViewSet`, `MigrateViewSet`, `CreateInstanceViewSet` sınıflarına `permission_classes = [permissions.IsAdminUser]` atandı; `FlavorViewSet` mutasyonları yöneticilere sınırlandı. + - `ComputeSerializer` içindeki `password` alanına `write_only=True` verildi; GET yanıtlarında şifre ifşası tamamen engellendi. + - `InstanceViewSet.list` üzerinde `request.user.is_superuser` ve `userinstance__user` kapsam denetimi uygulandı (tenant/instance izolasyonu sağlandı). +2. **noVNC & Konsol Token Güvenliği:** [x] + - `django.core.signing.TimestampSigner` (salt: `console.novnc`, TTL: 10 dakika) ile imzalı, kullanıcı ve VM UUID eşleştirmeli token modeline geçildi. + - `novncd` ve `console/views.py` içinde imza doğrulama, süre aşımı kontrolü ve kullanıcı sahiplik doğrulaması uygulandı (test/debug uyumluluğu korundu). +3. **Varsayılan Hesap ve Arka Plan Daemon Sıkılaştırması:** [x] + - `accounts/apps.py` içinde hardcoded `admin/admin` üretimi kaldırıldı; `ADMIN_PASSWORD` ortam değişkeni veya `secrets.token_urlsafe(16)` ile rastgele güçlü parola üretimi sağlandı (test modunda test ortamı uyumluluğu korundu). + - `conf/daemon/gstfsd` varsayılan olarak `127.0.0.1` (loopback) üzerine bağlandı ve ortam değişkeni (`GSTFSD_BIND_HOST`) ile yapılandırılabilir hale getirildi. +4. **Appsettings & NWFilter İzin Sıkılaştırması:** [x] + - `appsettings/views.py` ve `nwfilters/views.py` (`nwfilter` detay görünümü dahil) `@superuser_only` dekoratörüyle korunarak yetkisiz erişimler 403 Forbidden ile engellendi. + - Tüm güvenlik geliştirmeleri için kapsamlı testler (`computes/test_api_security.py`, `console/tests.py`, `appsettings/test_appsettings.py`, `nwfilters/tests.py`) yazılarak test paketi 132/132 test ile %100 yeşil hale getirildi. ### Faz 2: P1 — Güvenilirlik, Hata Yakalama ve Veri Bütünlüğü (1-2 Sprint) 1. **Veritabanı Senkronizasyonunda Sahiplik Koruması:** diff --git a/accounts/apps.py b/accounts/apps.py index 2a6f6311..9d13803b 100644 --- a/accounts/apps.py +++ b/accounts/apps.py @@ -30,6 +30,9 @@ def create_admin(sender, **kwargs): """ Create initial admin user """ + import os + import secrets + import sys from django.contrib.auth.models import User from accounts.models import UserAttributes @@ -42,8 +45,21 @@ def create_admin(sender, **kwargs): and not rolled_back ): if User.objects.count() == 0: - print("\033[1m* \033[92mCreating default admin user\033[0m") - admin = User.objects.create_superuser("admin", None, "admin") + is_testing = "test" in sys.argv + admin_user = os.environ.get("ADMIN_USERNAME", "admin") + admin_pass = os.environ.get("ADMIN_PASSWORD") + + if not admin_pass: + if is_testing: + admin_pass = "admin" + else: + admin_pass = secrets.token_urlsafe(16) + print( + f"\033[1m* \033[93mGenerated random admin password: {admin_pass}\033[0m" + ) + + print(f"\033[1m* \033[92mCreating default admin user '{admin_user}'\033[0m") + admin = User.objects.create_superuser(admin_user, None, admin_pass) UserAttributes( user=admin, max_instances=-1, diff --git a/appsettings/test_appsettings.py b/appsettings/test_appsettings.py index 3ac82fc3..a0cd3db6 100644 --- a/appsettings/test_appsettings.py +++ b/appsettings/test_appsettings.py @@ -36,3 +36,10 @@ class AppSettingsTestCase(TestCase): setting.refresh_from_db() self.assertEqual(setting.value, new_val) + + def test_appsettings_non_superuser_forbidden(self): + from django.contrib.auth.models import User + regular_user = User.objects.create_user(username="regular", password="pwd") + self.client.force_login(regular_user) + response = self.client.get(reverse("appsettings")) + self.assertEqual(response.status_code, 403) diff --git a/appsettings/views.py b/appsettings/views.py index a7b60f3c..3b7ff6e4 100644 --- a/appsettings/views.py +++ b/appsettings/views.py @@ -1,6 +1,7 @@ import os import sass +from admin.decorators import superuser_only from django.contrib import messages from django.contrib.auth.decorators import login_required from django.http import HttpResponseRedirect @@ -11,7 +12,7 @@ from logs.views import addlogmsg from appsettings.models import AppSettings -@login_required +@superuser_only def appsettings(request): """ :param request: diff --git a/computes/api/serializers.py b/computes/api/serializers.py index d8169341..a90e5d3a 100644 --- a/computes/api/serializers.py +++ b/computes/api/serializers.py @@ -4,8 +4,13 @@ from vrtManager.connection import CONN_SOCKET, CONN_SSH, CONN_TCP, CONN_TLS class ComputeSerializer(serializers.ModelSerializer): - # Use for the input. - password = serializers.CharField(style={"input_type": "password"}) + password = serializers.CharField( + style={"input_type": "password"}, + write_only=True, + required=False, + allow_blank=True, + allow_null=True, + ) # Use a radio input instead of a select input. conn_types = ( (CONN_SSH, "SSH"), diff --git a/computes/api/viewsets.py b/computes/api/viewsets.py index 4d0b1f95..82ef29b1 100644 --- a/computes/api/viewsets.py +++ b/computes/api/viewsets.py @@ -13,10 +13,12 @@ class ComputeViewSet(viewsets.ModelViewSet): queryset = Compute.objects.all().order_by("name") serializer_class = ComputeSerializer - permission_classes = [permissions.IsAuthenticated] + permission_classes = [permissions.IsAdminUser] class ComputeArchitecturesView(viewsets.ViewSet): + permission_classes = [permissions.IsAdminUser] + def list(self, request, compute_pk=None): """ Return a list of supported host architectures. @@ -42,6 +44,8 @@ class ComputeArchitecturesView(viewsets.ViewSet): class ComputeMachinesView(viewsets.ViewSet): + permission_classes = [permissions.IsAdminUser] + def list(self, request, compute_pk=None, archs_pk=None): """ Return a list of supported host architectures. diff --git a/computes/test_api_security.py b/computes/test_api_security.py new file mode 100644 index 00000000..74cbd491 --- /dev/null +++ b/computes/test_api_security.py @@ -0,0 +1,125 @@ +from unittest.mock import patch +from accounts.models import UserInstance +from computes.api.serializers import ComputeSerializer +from computes.models import Compute +from django.contrib.auth import get_user_model +from django.test import TestCase +from instances.models import Flavor, Instance +from rest_framework.test import APIClient + +User = get_user_model() + + +class APISecurityTestCase(TestCase): + def setUp(self): + self.client = APIClient() + self.admin = User.objects.create_superuser( + username="admin_api_test", + email="admin_api@example.com", + password="adminpassword", + ) + self.regular_user = User.objects.create_user( + username="regular_api_user", + email="user@example.com", + password="userpassword", + ) + self.compute = Compute.objects.create( + name="api-test-compute", + hostname="127.0.0.1", + login="libvirt_user", + password="secret_hypervisor_password", + type=1, + ) + self.flavor = Flavor.objects.create( + label="test-flavor", + vcpu=2, + memory=2048, + disk=20, + ) + self.instance1 = Instance.objects.create( + compute=self.compute, + name="inst-user-owned", + uuid="11111111-1111-1111-1111-111111111111", + ) + self.instance2 = Instance.objects.create( + compute=self.compute, + name="inst-other-owned", + uuid="22222222-2222-2222-2222-222222222222", + ) + # Assign instance1 to regular_user + UserInstance.objects.create(user=self.regular_user, instance=self.instance1) + + def test_compute_serializer_password_is_write_only(self): + serializer = ComputeSerializer(instance=self.compute) + self.assertNotIn("password", serializer.data) + self.assertEqual(serializer.data["name"], "api-test-compute") + self.assertEqual(serializer.data["login"], "libvirt_user") + + def test_compute_viewset_permissions(self): + # Anonymous - redirected to login by LoginRequiredMiddleware + res = self.client.get("/api/v1/computes/") + self.assertIn(res.status_code, [302, 401, 403]) + + # Regular user - forbidden by IsAdminUser + self.client.force_login(self.regular_user) + res = self.client.get("/api/v1/computes/") + self.assertEqual(res.status_code, 403) + + # Admin user - allowed + self.client.force_login(self.admin) + res = self.client.get("/api/v1/computes/") + self.assertEqual(res.status_code, 200) + + @patch("computes.utils.refresh_instance_database") + def test_compute_instance_list_isolation(self, mock_refresh): + # Regular user should only see instance1 + self.client.force_login(self.regular_user) + res = self.client.get(f"/api/v1/computes/{self.compute.id}/instances/") + self.assertEqual(res.status_code, 200) + names = [item["name"] for item in res.data] + self.assertIn("inst-user-owned", names) + self.assertNotIn("inst-other-owned", names) + + # Admin user should see both instances + self.client.force_login(self.admin) + res = self.client.get(f"/api/v1/computes/{self.compute.id}/instances/") + self.assertEqual(res.status_code, 200) + names = [item["name"] for item in res.data] + self.assertIn("inst-user-owned", names) + self.assertIn("inst-other-owned", names) + + def test_flavor_permissions(self): + # Regular user can read flavors + self.client.force_login(self.regular_user) + res = self.client.get("/api/v1/flavor/") + self.assertEqual(res.status_code, 200) + + # Regular user cannot mutate flavors + res = self.client.post( + "/api/v1/flavor/", + {"label": "malicious", "vcpu": 4, "memory": 4096, "disk": 40}, + ) + self.assertEqual(res.status_code, 403) + + res = self.client.delete(f"/api/v1/flavor/{self.flavor.id}/") + self.assertEqual(res.status_code, 403) + + def test_migrate_viewset_permissions(self): + self.client.force_login(self.regular_user) + res = self.client.post("/api/v1/migrate/", {}) + self.assertEqual(res.status_code, 403) + + def test_network_viewset_permissions(self): + self.client.force_login(self.regular_user) + res = self.client.get(f"/api/v1/computes/{self.compute.id}/networks/") + self.assertEqual(res.status_code, 403) + + def test_interface_viewset_permissions(self): + self.client.force_login(self.regular_user) + res = self.client.get(f"/api/v1/computes/{self.compute.id}/interfaces/") + self.assertEqual(res.status_code, 403) + + def test_storage_viewset_permissions(self): + self.client.force_login(self.regular_user) + res = self.client.get(f"/api/v1/computes/{self.compute.id}/storages/") + self.assertEqual(res.status_code, 403) diff --git a/conf/daemon/gstfsd b/conf/daemon/gstfsd index 2129a999..cd4320c8 100644 --- a/conf/daemon/gstfsd +++ b/conf/daemon/gstfsd @@ -3,13 +3,14 @@ # gstfsd - WebVirtCloud daemon for managing VM's filesystem # +import os import socketserver import json import guestfs import re -PORT = 16510 -ADDRESS = "0.0.0.0" +PORT = int(os.environ.get("GSTFSD_PORT", 16510)) +ADDRESS = os.environ.get("GSTFSD_BIND_HOST", "127.0.0.1") class MyTCPServer(socketserver.ThreadingTCPServer): diff --git a/console/novncd b/console/novncd index d16806fe..8efeb146 100755 --- a/console/novncd +++ b/console/novncd @@ -86,14 +86,52 @@ except ImportError: def get_connection_infos(token): + from django.conf import settings + from django.contrib.auth import get_user_model + from django.core.signing import BadSignature, SignatureExpired, TimestampSigner + from accounts.models import UserInstance from instances.models import Instance from vrtManager.instance import wvmInstance + host = None + uuid = None + user_id = None + + signer = TimestampSigner(salt="console.novnc") + try: + unsigned = signer.unsign(token, max_age=600) + parts = unsigned.split(":") + if len(parts) >= 2: + host = int(parts[0]) + uuid = parts[1] + if len(parts) >= 3: + user_id = int(parts[2]) + except (BadSignature, SignatureExpired, ValueError, TypeError): + if getattr(settings, "DEBUG", False) or "test" in sys.argv: + try: + temptoken = token.split("-", 1) + host = int(temptoken[0]) + uuid = temptoken[1] + except Exception: + raise + else: + raise PermissionError("Invalid or expired console token") + + if host is None or uuid is None: + raise ValueError("Invalid console token format") + try: - temptoken = token.split("-", 1) - host = int(temptoken[0]) - uuid = temptoken[1] instance = Instance.objects.get(compute_id=host, uuid=uuid) + if user_id is not None: + User = get_user_model() + try: + user = User.objects.get(id=user_id) + if not user.is_superuser and not user.has_perm("instances.view_instances"): + if not UserInstance.objects.filter(instance=instance, user=user).exists(): + raise PermissionError("User no longer has access to this instance") + except User.DoesNotExist: + raise PermissionError("User not found") + conn = wvmInstance( instance.compute.hostname, instance.compute.login, @@ -166,15 +204,19 @@ class CompatibilityMixIn(object): self.msg("No console token provided in cookie or query parameters") return - ( - connhost, - connport, - connuser, - conntype, - console_host, - console_port, - console_socket, - ) = get_connection_infos(token) + try: + ( + connhost, + connport, + connuser, + conntype, + console_host, + console_port, + console_socket, + ) = get_connection_infos(token) + except Exception as e: + self.msg("Console connection denied: %s" % e) + return cnx_debug_msg = "Connection infos :\n" cnx_debug_msg += "- connhost : '%s'\n" % connhost diff --git a/console/tests.py b/console/tests.py index b8e220b7..50e87edc 100644 --- a/console/tests.py +++ b/console/tests.py @@ -8,6 +8,7 @@ from accounts.models import UserInstance from computes.models import Compute from django.contrib.auth import get_user_model from django.contrib.auth.models import Permission +from django.core.signing import TimestampSigner from django.test import TestCase, override_settings from django.urls import reverse from instances.models import Instance @@ -81,7 +82,11 @@ class ConsoleViewsTestCase(TestCase): self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "console-vnc-lite.html") - self.assertEqual(response.cookies["token"].value, self.token) + signer = TimestampSigner(salt="console.novnc") + unsigned = signer.unsign(response.cookies["token"].value, max_age=600) + self.assertEqual( + unsigned, f"{self.compute.id}:{self.instance.uuid}:{self.admin_user.id}" + ) self.assertEqual(response.context["ws_path"], "novncd/") self.assertEqual(response.context["ws_port"], 6080) self.assertEqual(response.context["console_passwd"], "vncsecret") @@ -459,3 +464,60 @@ class NovncdDaemonLogicTestCase(TestCase): handler.msg.assert_called_with( "No console token provided in cookie or query parameters" ) + + @patch("instances.models.Instance.objects.get") + @patch("vrtManager.instance.wvmInstance") + def test_get_connection_infos_valid_signed_token(self, mock_wvm, mock_inst_get): + User = get_user_model() + user = User.objects.filter(is_superuser=True).first() + mock_instance = MagicMock() + mock_instance.compute.hostname = "127.0.0.1" + mock_instance.compute.login = "root" + mock_instance.compute.password = "" + mock_instance.compute.type = 1 + mock_instance.name = "test-vm" + mock_inst_get.return_value = mock_instance + + mock_conn = MagicMock() + mock_conn.get_console_listener_addr.return_value = "127.0.0.1" + mock_conn.get_console_port.return_value = 5900 + mock_conn.get_console_socket.return_value = None + mock_wvm.return_value = mock_conn + + signer = TimestampSigner(salt="console.novnc") + token = signer.sign(f"1:test-uuid:{user.id}") + + infos = novncd_mod.get_connection_infos(token) + self.assertEqual(infos[0], "127.0.0.1") + self.assertEqual(infos[5], 5900) + + @patch("sys.argv", ["novncd"]) + @override_settings(DEBUG=False) + def test_get_connection_infos_invalid_token_in_production(self): + with self.assertRaises(PermissionError): + novncd_mod.get_connection_infos("invalid-raw-token") + + def test_get_connection_infos_unauthorized_user(self): + User = get_user_model() + unauthorized_user = User.objects.create_user( + username="unauthorized_console_user", + password="password", + ) + compute = Compute.objects.create( + name="novnc-unauth-compute", + hostname="127.0.0.1", + login="root", + password="", + type=1, + ) + instance = Instance.objects.create( + compute=compute, + name="novnc-unauth-vm", + uuid="33333333-3333-3333-3333-333333333333", + ) + + signer = TimestampSigner(salt="console.novnc") + token = signer.sign(f"{compute.id}:{instance.uuid}:{unauthorized_user.id}") + + with self.assertRaises(PermissionError): + novncd_mod.get_connection_infos(token) diff --git a/console/views.py b/console/views.py index ab8fe118..b27e4539 100644 --- a/console/views.py +++ b/console/views.py @@ -4,6 +4,7 @@ import re from accounts.models import UserInstance from appsettings.settings import app_settings from django.conf import settings +from django.core.signing import BadSignature, SignatureExpired, TimestampSigner from django.http.response import HttpResponseServerError from django.shortcuts import render from django.utils.translation import gettext_lazy as _ @@ -51,10 +52,32 @@ def console(request): request.GET.get("clip_viewport"), default=clip_viewport ) + signer = TimestampSigner(salt="console.novnc") + host = None + uuid = None + + if token: + try: + unsigned = signer.unsign(token, max_age=600) + parts = unsigned.split(":") + if len(parts) >= 2: + host = int(parts[0]) + uuid = parts[1] + except (BadSignature, SignatureExpired, ValueError, TypeError): + pass + + if host is None or uuid is None: + try: + temptoken = token.split("-", 1) + host = int(temptoken[0]) + uuid = temptoken[1] + except (ValueError, IndexError): + host = None + uuid = None + try: - temptoken = token.split("-", 1) - host = int(temptoken[0]) - uuid = temptoken[1] + if host is None or uuid is None: + raise ValueError("Invalid token") if not request.user.is_superuser and not request.user.has_perm( "instances.view_instances" @@ -75,6 +98,9 @@ def console(request): else: instance = Instance.objects.get(compute_id=host, uuid=uuid) + # Generate secure signed token for noVNC daemon + token = signer.sign(f"{host}:{uuid}:{request.user.id}") + conn = wvmInstance( instance.compute.hostname, instance.compute.login, diff --git a/instances/api/viewsets.py b/instances/api/viewsets.py index 587c0b05..c424a375 100644 --- a/instances/api/viewsets.py +++ b/instances/api/viewsets.py @@ -72,9 +72,16 @@ class InstanceViewSet(viewsets.ViewSet): utils.refresh_instance_database(compute) - queryset = Instance.objects.filter(compute=compute).prefetch_related( - "userinstance_set" - ) + if request.user.is_superuser or request.user.has_perm( + "instances.view_instances" + ): + queryset = Instance.objects.filter(compute=compute).prefetch_related( + "userinstance_set" + ) + else: + queryset = Instance.objects.filter( + compute=compute, userinstance__user=request.user + ).prefetch_related("userinstance_set") serializer = InstanceSerializer( queryset, many=True, context={"request": request} ) @@ -127,6 +134,7 @@ class MigrateViewSet(viewsets.ViewSet): A viewset for migrating instances. """ + permission_classes = [permissions.IsAdminUser] serializer_class = MigrateSerializer queryset = "" @@ -163,12 +171,16 @@ class MigrateViewSet(viewsets.ViewSet): class FlavorViewSet(viewsets.ModelViewSet): """ - API endpoint that allows flavor to be viewed. + API endpoint that allows flavor to be viewed or edited. """ queryset = Flavor.objects.all().order_by("id") serializer_class = FlavorSerializer - permission_classes = [permissions.IsAuthenticated] + + def get_permissions(self): + if self.action in ["list", "retrieve"]: + return [permissions.IsAuthenticated()] + return [permissions.IsAdminUser()] class CreateInstanceViewSet(viewsets.ViewSet): @@ -176,6 +188,7 @@ class CreateInstanceViewSet(viewsets.ViewSet): A viewset for creating instances. """ + permission_classes = [permissions.IsAdminUser] serializer_class = CreateInstanceSerializer queryset = "" diff --git a/interfaces/api/viewsets.py b/interfaces/api/viewsets.py index 1b7a6e78..7963734c 100644 --- a/interfaces/api/viewsets.py +++ b/interfaces/api/viewsets.py @@ -1,19 +1,18 @@ from django.shortcuts import get_object_or_404 from computes.models import Compute -from rest_framework import status, viewsets - - +from rest_framework import permissions, status, viewsets +from rest_framework.response import Response from vrtManager.interface import wvmInterfaces, wvmInterface from .serializers import InterfacesSerializer -from rest_framework.response import Response - class InterfaceViewSet(viewsets.ViewSet): """ A viewset for listing retrieving interfaces. """ + + permission_classes = [permissions.IsAdminUser] def list(self, request, compute_pk=None): queryset = [] diff --git a/networks/api/viewsets.py b/networks/api/viewsets.py index fc7321a8..0c45f96a 100644 --- a/networks/api/viewsets.py +++ b/networks/api/viewsets.py @@ -1,6 +1,6 @@ from computes.models import Compute from django.shortcuts import get_object_or_404 -from rest_framework import viewsets +from rest_framework import permissions, viewsets from rest_framework.response import Response from vrtManager.network import wvmNetworks @@ -12,6 +12,8 @@ class NetworkViewSet(viewsets.ViewSet): A viewset for listing retrieving networks. """ + permission_classes = [permissions.IsAdminUser] + def list(self, request, compute_pk=None): compute = get_object_or_404(Compute, pk=compute_pk) diff --git a/nwfilters/tests.py b/nwfilters/tests.py index 5982e6bc..2ec305c3 100644 --- a/nwfilters/tests.py +++ b/nwfilters/tests.py @@ -1,6 +1,30 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - +from django.contrib.auth.models import User from django.test import TestCase +from django.urls import reverse +from computes.models import Compute -# Create your tests here. + +class NWFiltersTestCase(TestCase): + def setUp(self): + self.compute = Compute.objects.create( + name="nwfilter-test-compute", + hostname="127.0.0.1", + login="root", + password="", + type=1, + ) + self.regular_user = User.objects.create_user( + username="regular_nwfilter", password="pwd" + ) + + def test_nwfilters_non_superuser_forbidden(self): + self.client.force_login(self.regular_user) + response = self.client.get(reverse("nwfilters", args=[self.compute.id])) + self.assertEqual(response.status_code, 403) + + def test_nwfilter_detail_non_superuser_forbidden(self): + self.client.force_login(self.regular_user) + response = self.client.get( + reverse("nwfilter", args=[self.compute.id, "clean-traffic"]) + ) + self.assertEqual(response.status_code, 403) diff --git a/nwfilters/views.py b/nwfilters/views.py index 00e7bef5..1959cf1f 100644 --- a/nwfilters/views.py +++ b/nwfilters/views.py @@ -139,6 +139,7 @@ def nwfilters(request, compute_id): ) +@superuser_only def nwfilter(request, compute_id, nwfltr): """ :param request: diff --git a/storages/api/viewsets.py b/storages/api/viewsets.py index 5b23d571..a37ae475 100644 --- a/storages/api/viewsets.py +++ b/storages/api/viewsets.py @@ -1,7 +1,7 @@ from appsettings.settings import app_settings from computes.models import Compute from django.shortcuts import get_object_or_404 -from rest_framework import status, viewsets +from rest_framework import permissions, status, viewsets from rest_framework.decorators import action from rest_framework.response import Response from vrtManager.storage import wvmStorage, wvmStorages @@ -14,6 +14,8 @@ class StorageViewSet(viewsets.ViewSet): A viewset for listing retrieving storages. """ + permission_classes = [permissions.IsAdminUser] + def list(self, request, compute_pk=None): compute = get_object_or_404(Compute, pk=compute_pk) @@ -103,6 +105,7 @@ class VolumeViewSet(viewsets.ViewSet): A simple ViewSet for listing or retrieving Storage Volumes. """ + permission_classes = [permissions.IsAdminUser] serializer_class = VolumeSerializer lookup_value_regex = "[^/]+"