1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-09 19:56:48 +00:00

feat(security): implement P0 critical security fixes for API, console, and access control

- Computes API: mark password as write_only in ComputeSerializer, restrict ComputeViewSet and architectures to IsAdminUser.
- Instances API: enforce user ownership tenant isolation in InstanceViewSet.list, restrict MigrateViewSet and CreateInstanceViewSet to IsAdminUser, require IsAdminUser for mutating FlavorViewSet.
- Storage, Network, and Interface APIs: restrict StorageViewSet, VolumeViewSet, NetworkViewSet, InterfaceViewSet to IsAdminUser.
- AppSettings & NWFilters: protect appsettings and nwfilter views with @superuser_only (403 Forbidden for non-superusers).
- noVNC Console: sign tokens with TimestampSigner (salt='console.novnc', max_age=600), verify signature and user authorization in novncd with backward-compatible fallback for debug/test mode.
- Accounts: avoid hardcoded default admin password; generate cryptographically secure random password unless ADMIN_PASSWORD is set (preserve 'admin' in test runner).
- gstfsd daemon: bind to 127.0.0.1 by default instead of 0.0.0.0.
- Tests: added comprehensive test coverage for API security, token expiration/rejection, and view permissions (132 tests passing).
This commit is contained in:
catborise 2026-09-27 10:50:09 +03:00
parent 1c561de4d2
commit 1227674d8f
17 changed files with 385 additions and 53 deletions

View file

@ -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ı:**

View file

@ -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,

View file

@ -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)

View file

@ -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:

View file

@ -4,8 +4,13 @@ from vrtManager.connection import CONN_SOCKET, CONN_SSH, CONN_TCP, CONN_TLS
class ComputeSerializer(serializers.ModelSerializer):
# Use <input type="password"> 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"),

View file

@ -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.

View file

@ -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)

View file

@ -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):

View file

@ -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

View file

@ -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)

View file

@ -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,

View file

@ -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 = ""

View file

@ -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 = []

View file

@ -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)

View file

@ -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)

View file

@ -139,6 +139,7 @@ def nwfilters(request, compute_id):
)
@superuser_only
def nwfilter(request, compute_id, nwfltr):
"""
:param request:

View file

@ -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 = "[^/]+"