mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2026-07-04 20:11:45 +02:00
feat: add demo mode tests to ensure API is disabled on it
This commit is contained in:
@@ -0,0 +1,155 @@
|
|||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.test import TestCase, override_settings
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils import timezone
|
||||||
|
from oauth2_provider.models import get_access_token_model, get_application_model
|
||||||
|
|
||||||
|
from apps.users.models import APIToken
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
Application = get_application_model()
|
||||||
|
AccessToken = get_access_token_model()
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(DEMO=True)
|
||||||
|
class DemoModeAPITests(TestCase):
|
||||||
|
"""The DEMO-mode gate (apps.api.permissions.NotInDemoMode) must reject
|
||||||
|
API access regardless of the authentication method used, including the
|
||||||
|
PAT and OAuth2 backends introduced for MCP integrations."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.user = User.objects.create_user(
|
||||||
|
email="demo@example.com",
|
||||||
|
password="test-password",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_pat_cannot_access_api_in_demo_mode(self):
|
||||||
|
_token, raw_token = APIToken.objects.create_token(
|
||||||
|
user=self.user, name="n8n"
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
"/api/accounts/",
|
||||||
|
HTTP_AUTHORIZATION=f"Token {raw_token}",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
def test_oauth_access_token_cannot_access_api_in_demo_mode(self):
|
||||||
|
app = Application.objects.create(
|
||||||
|
name="Test Client",
|
||||||
|
client_type=Application.CLIENT_CONFIDENTIAL,
|
||||||
|
authorization_grant_type=Application.GRANT_AUTHORIZATION_CODE,
|
||||||
|
redirect_uris="http://127.0.0.1:8765/callback",
|
||||||
|
client_secret="secret",
|
||||||
|
)
|
||||||
|
access_token = AccessToken.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
scope="mcp",
|
||||||
|
expires=timezone.now() + timedelta(hours=1),
|
||||||
|
token="demo-oauth-access-token-xyz",
|
||||||
|
application=app,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
"/api/accounts/",
|
||||||
|
HTTP_AUTHORIZATION=f"Bearer {access_token.token}",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
def test_superuser_pat_can_access_api_in_demo_mode(self):
|
||||||
|
admin = User.objects.create_superuser(
|
||||||
|
email="admin@example.com",
|
||||||
|
password="test-password",
|
||||||
|
)
|
||||||
|
_token, raw_token = APIToken.objects.create_token(
|
||||||
|
user=admin, name="admin"
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
"/api/accounts/",
|
||||||
|
HTTP_AUTHORIZATION=f"Token {raw_token}",
|
||||||
|
)
|
||||||
|
|
||||||
|
# NotInDemoMode grants superusers access in DEMO mode; the request is
|
||||||
|
# authenticated by the PAT, so the API responds normally (never 403).
|
||||||
|
self.assertNotEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(DEMO=True)
|
||||||
|
class DemoModeOAuthEndpointTests(TestCase):
|
||||||
|
"""OAuth2 issuance and discovery endpoints must be disabled in DEMO mode
|
||||||
|
so demo tenants cannot obtain (or even discover) credentials."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.user = User.objects.create_user(
|
||||||
|
email="demo@example.com",
|
||||||
|
password="test-password",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_oauth_authorize_rejects_non_superuser_in_demo_mode(self):
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
response = self.client.get(reverse("oauth2_provider:authorize"))
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
def test_oauth_token_rejects_non_superuser_in_demo_mode(self):
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
response = self.client.post(reverse("oauth2_provider:token"))
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
def test_oauth_authorization_server_metadata_rejects_in_demo_mode(self):
|
||||||
|
response = self.client.get(reverse("oauth-authorization-server-metadata"))
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
def test_oauth_dynamic_client_registration_rejects_in_demo_mode(self):
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("oauth-dynamic-client-registration"),
|
||||||
|
data="{}",
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(DEMO=True)
|
||||||
|
class DemoModeAPITokenViewsTests(TestCase):
|
||||||
|
"""The PAT management UI must be disabled in DEMO mode just like the
|
||||||
|
other mutating user views."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.user = User.objects.create_user(
|
||||||
|
email="demo@example.com",
|
||||||
|
password="test-password",
|
||||||
|
)
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
self.htmx_headers = {"HTTP_HX_REQUEST": "true"}
|
||||||
|
|
||||||
|
def test_cannot_create_api_token_from_ui_in_demo_mode(self):
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("user_api_token_add"),
|
||||||
|
{"name": "n8n", "expires_in_days": "30"},
|
||||||
|
**self.htmx_headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
self.assertEqual(APIToken.objects.count(), 0)
|
||||||
|
|
||||||
|
def test_cannot_revoke_api_token_from_ui_in_demo_mode(self):
|
||||||
|
token, _ = APIToken.objects.create_token(user=self.user, name="n8n")
|
||||||
|
|
||||||
|
response = self.client.delete(
|
||||||
|
reverse("user_api_token_revoke", kwargs={"token_id": token.id}),
|
||||||
|
**self.htmx_headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
token.refresh_from_db()
|
||||||
|
self.assertIsNone(token.revoked_at)
|
||||||
@@ -34,8 +34,7 @@
|
|||||||
{% if demo_mode %}
|
{% if demo_mode %}
|
||||||
<div class="px-3 m-0" id="demo-mode-alert" hx-preserve>
|
<div class="px-3 m-0" id="demo-mode-alert" hx-preserve>
|
||||||
<div class="alert alert-warning my-3 relative" role="alert">
|
<div class="alert alert-warning my-3 relative" role="alert">
|
||||||
<strong>{% trans "This is a demo!" %}</strong> {% trans "Any data you add here will be wiped in 24hrs or less"
|
<strong>{% trans "This is a demo!" %}</strong>{% trans "Any data you add here will be wiped in 24hrs or less" %}
|
||||||
%}
|
|
||||||
<button type="button" class="btn btn-sm btn-ghost absolute right-2 top-1/2 -translate-y-1/2"
|
<button type="button" class="btn btn-sm btn-ghost absolute right-2 top-1/2 -translate-y-1/2"
|
||||||
onclick="this.parentElement.style.display='none'" aria-label="Close">✕</button>
|
onclick="this.parentElement.style.display='none'" aria-label="Close">✕</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user