mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 11:16:47 +00:00
fix(api): schema and API docs need a login, and DRF views deny anonymous by default
This commit is contained in:
parent
f889861e18
commit
841c412f76
4 changed files with 45 additions and 12 deletions
|
|
@ -492,6 +492,12 @@ Environment-driven database and email settings: copy the `DATABASES` and
|
|||
`EMAIL` blocks of the template into your `webvirtcloud/settings.py` to use them.
|
||||
PostgreSQL is for new installs; moving an existing sqlite database is not covered.
|
||||
|
||||
The API docs (`/swagger/`, `/redoc/`, `/api/schema/`) now need a login: add
|
||||
`"SERVE_PERMISSIONS": ["rest_framework.permissions.IsAuthenticated"]` to
|
||||
`SPECTACULAR_SETTINGS` and `"DEFAULT_PERMISSION_CLASSES":
|
||||
["rest_framework.permissions.IsAuthenticated"]` to `REST_FRAMEWORK` in your
|
||||
`webvirtcloud/settings.py`.
|
||||
|
||||
```bash
|
||||
# Go to Installation Directory
|
||||
cd /srv/webvirtcloud
|
||||
|
|
@ -630,7 +636,7 @@ If you'd like to move a user from ldap to WebVirtCloud, just change its password
|
|||
|
||||
WebVirtCloud provides a REST API powered by Django REST Framework and documented via `drf-spectacular`.
|
||||
|
||||
You can access the interactive API documentation and schema endpoints in your browser:
|
||||
You can access the interactive API documentation and schema endpoints in your browser, after logging in:
|
||||
|
||||
* **Swagger UI:** `http://<webvirtcloud-address:port>/swagger/`
|
||||
* **ReDoc UI:** `http://<webvirtcloud-address:port>/redoc/`
|
||||
|
|
|
|||
|
|
@ -357,6 +357,10 @@ REST_FRAMEWORK = {
|
|||
"DEFAULT_AUTHENTICATION_CLASSES": [
|
||||
"rest_framework.authentication.SessionAuthentication",
|
||||
],
|
||||
# A view that forgets its own permission_classes is not public.
|
||||
"DEFAULT_PERMISSION_CLASSES": [
|
||||
"rest_framework.permissions.IsAuthenticated",
|
||||
],
|
||||
}
|
||||
|
||||
SPECTACULAR_SETTINGS = {
|
||||
|
|
@ -364,6 +368,7 @@ SPECTACULAR_SETTINGS = {
|
|||
"DESCRIPTION": "Webvirtcloud REST API",
|
||||
"VERSION": "v1",
|
||||
"SERVE_INCLUDE_SCHEMA": False,
|
||||
"SERVE_PERMISSIONS": ["rest_framework.permissions.IsAuthenticated"],
|
||||
"SWAGGER_UI_DIST": "SIDECAR",
|
||||
"SWAGGER_UI_FAVICON_HREF": "SIDECAR",
|
||||
"REDOC_DIST": "SIDECAR",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,11 @@ import os
|
|||
from django.contrib.auth import get_user_model
|
||||
from django.core.management import call_command
|
||||
from django.test import TestCase
|
||||
from rest_framework.test import APIClient
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.test import APIClient, APIRequestFactory
|
||||
from rest_framework.views import APIView
|
||||
|
||||
SCHEMA_URLS = ["/api/schema/", "/swagger.json", "/swagger/", "/redoc/"]
|
||||
|
||||
|
||||
class APISchemaTestCase(TestCase):
|
||||
|
|
@ -16,3 +20,25 @@ class APISchemaTestCase(TestCase):
|
|||
client.force_login(admin)
|
||||
self.assertEqual(client.get("/api/v1/computes/abc/instances/").status_code, 404)
|
||||
self.assertEqual(client.get("/api/v1/instances/abc/").status_code, 404)
|
||||
|
||||
|
||||
class SchemaPermissionTestCase(TestCase):
|
||||
def test_schema_requires_login(self):
|
||||
client = APIClient()
|
||||
for url in SCHEMA_URLS:
|
||||
self.assertEqual(client.get(url).status_code, 403, url)
|
||||
|
||||
def test_any_user_may_read_the_schema(self):
|
||||
user = get_user_model().objects.create_user("schema_reader", password="pw")
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
for url in SCHEMA_URLS:
|
||||
self.assertEqual(client.get(url).status_code, 200, url)
|
||||
|
||||
def test_view_without_permission_classes_denies_anonymous(self):
|
||||
class Probe(APIView):
|
||||
def get(self, request):
|
||||
return Response({})
|
||||
|
||||
response = Probe.as_view()(APIRequestFactory().get("/probe/"))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ from django.contrib.auth import get_user_model
|
|||
from django.contrib.auth.models import Permission
|
||||
from django.db import transaction
|
||||
from django.test import Client, TestCase
|
||||
from django.urls import NoReverseMatch, URLPattern, URLResolver, get_resolver, reverse
|
||||
from django.urls import NoReverseMatch, URLPattern, URLResolver, get_resolver, resolve, reverse
|
||||
|
||||
from accounts.models import UserInstance, UserSSHKey
|
||||
from computes.models import Compute
|
||||
|
|
@ -46,10 +46,10 @@ ALLOWED_FOR_ANY_USER = {
|
|||
"compute-instance-list": "API: filtered to the user's own instances",
|
||||
"instance-flavor-list": "API: flavor catalogue",
|
||||
"instance-flavor-detail": "API: flavor catalogue",
|
||||
"schema": "OpenAPI schema (SERVE_PERMISSIONS not restricted yet)",
|
||||
"schema-json": "OpenAPI schema (SERVE_PERMISSIONS not restricted yet)",
|
||||
"schema-redoc": "API docs (SERVE_PERMISSIONS not restricted yet)",
|
||||
"schema-swagger-ui": "API docs (SERVE_PERMISSIONS not restricted yet)",
|
||||
"schema": "API docs: any authenticated user (SERVE_PERMISSIONS)",
|
||||
"schema-json": "API docs: any authenticated user (SERVE_PERMISSIONS)",
|
||||
"schema-redoc": "API docs: any authenticated user (SERVE_PERMISSIONS)",
|
||||
"schema-swagger-ui": "API docs: any authenticated user (SERVE_PERMISSIONS)",
|
||||
}
|
||||
|
||||
# URL name -> why an anonymous visitor may reach it. Everything else must
|
||||
|
|
@ -60,10 +60,6 @@ ALLOWED_FOR_ANONYMOUS = {
|
|||
"ds_openstack_index": "cloud-init datasource, fetched by VMs that do not log in",
|
||||
"ds_openstack_metadata": "cloud-init datasource, fetched by VMs that do not log in",
|
||||
"ds_openstack_userdata": "cloud-init datasource, fetched by VMs that do not log in",
|
||||
"schema": "OpenAPI schema (SERVE_PERMISSIONS not restricted yet)",
|
||||
"schema-json": "OpenAPI schema (SERVE_PERMISSIONS not restricted yet)",
|
||||
"schema-redoc": "API docs (SERVE_PERMISSIONS not restricted yet)",
|
||||
"schema-swagger-ui": "API docs (SERVE_PERMISSIONS not restricted yet)",
|
||||
}
|
||||
|
||||
# Endpoints that expose a VM's console (or its VNC password) and therefore
|
||||
|
|
@ -234,7 +230,7 @@ class AuthorizationMatrixTestCase(TestCase):
|
|||
response = method(url)
|
||||
# DRF views opt out of login middleware and deny by their own
|
||||
# permission classes.
|
||||
api_denied = url.startswith("/api/") and response.status_code in (401, 403)
|
||||
api_denied = hasattr(resolve(url).func, "cls") and response.status_code in (401, 403)
|
||||
to_login = response.status_code == 302 and response["Location"].startswith(settings.LOGIN_URL)
|
||||
if not (api_denied or to_login):
|
||||
reachable.append(f"{method.__name__.upper()} {name} ({url}) -> {response.status_code}")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue