mirror of
https://github.com/inventree/InvenTree.git
synced 2026-07-04 14:10:52 +00:00
Permissions fix (#12168)
* Tighten API permissions - Require authenticated user for NotFoundView - Hide 'active_plugins' behind is_authenticated * Patch permissions hole in GlobalSettingsPermissions * Additional API unit tests * Require auth for observability endpoint * Add explicit permission for PluginAdminDetail * Bump API version * Update unit tests * Revert changes
This commit is contained in:
@@ -15,7 +15,7 @@ from django.views.generic.base import RedirectView
|
||||
import structlog
|
||||
from django_q.models import OrmQ
|
||||
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
|
||||
from rest_framework import serializers, viewsets
|
||||
from rest_framework import permissions, serializers, viewsets
|
||||
from rest_framework.generics import GenericAPIView
|
||||
from rest_framework.request import clone_request
|
||||
from rest_framework.response import Response
|
||||
@@ -356,7 +356,10 @@ class InfoView(APIView):
|
||||
class NotFoundView(APIView):
|
||||
"""Simple JSON view when accessing an invalid API view."""
|
||||
|
||||
permission_classes = [InvenTree.permissions.AllowAnyOrReadScope]
|
||||
permission_classes = [
|
||||
permissions.IsAuthenticated,
|
||||
InvenTree.permissions.AllowAnyOrReadScope,
|
||||
]
|
||||
|
||||
def not_found(self, request):
|
||||
"""Return a 404 error."""
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
"""InvenTree API version information."""
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 505
|
||||
INVENTREE_API_VERSION = 506
|
||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||
|
||||
INVENTREE_API_TEXT = """
|
||||
|
||||
v506 -> 2026-06-15 : https://github.com/inventree/InvenTree/pull/12168
|
||||
- Reduce permissions scope for a number of API endpoints, to improve security and ensure that users only have access to the data they need
|
||||
|
||||
v505 -> 2026-06-15 : https://github.com/inventree/InvenTree/pull/12165
|
||||
- Allow parameters to be specified against the PartCategory model
|
||||
|
||||
|
||||
@@ -415,7 +415,6 @@ class GlobalSettingsPermissions(OASTokenMixin, permissions.BasePermission):
|
||||
"""Check that the requesting user is 'admin'."""
|
||||
try:
|
||||
user = request.user
|
||||
|
||||
if request.method in permissions.SAFE_METHODS:
|
||||
return True
|
||||
# Any other methods require staff access permissions
|
||||
|
||||
@@ -612,6 +612,7 @@ class GeneralApiTests(InvenTreeAPITestCase):
|
||||
response = self.get(
|
||||
url, headers={'Authorization': f'Token {token}'}, max_query_count=20
|
||||
)
|
||||
self.assertIsNotNone(data.get('active_plugins'))
|
||||
self.assertGreater(len(response.json()['database']), 4)
|
||||
|
||||
data = response.json()
|
||||
|
||||
@@ -1405,7 +1405,7 @@ class ObservabilityEndSerializer(serializers.Serializer):
|
||||
class ObservabilityEnd(CreateAPI):
|
||||
"""Endpoint for observability tools."""
|
||||
|
||||
permission_classes = [AllowAnyOrReadScope]
|
||||
permission_classes = [IsAuthenticated, AllowAnyOrReadScope]
|
||||
serializer_class = ObservabilityEndSerializer
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
|
||||
@@ -10,7 +10,7 @@ import django_filters.rest_framework.filters as rest_filters
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from django_filters.rest_framework.filterset import FilterSet
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from rest_framework import status
|
||||
from rest_framework import permissions, status
|
||||
from rest_framework.exceptions import NotFound
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
@@ -174,7 +174,10 @@ class PluginDetail(RetrieveDestroyAPI):
|
||||
|
||||
queryset = PluginConfig.objects.all()
|
||||
serializer_class = PluginSerializers.PluginConfigSerializer
|
||||
permission_classes = [InvenTree.permissions.IsSuperuserOrReadOnlyOrScope]
|
||||
permission_classes = [
|
||||
permissions.IsAuthenticated,
|
||||
InvenTree.permissions.IsSuperuserOrReadOnlyOrScope,
|
||||
]
|
||||
lookup_field = 'key'
|
||||
lookup_url_kwarg = 'plugin'
|
||||
|
||||
@@ -201,6 +204,7 @@ class PluginAdminDetail(RetrieveAPI):
|
||||
|
||||
queryset = PluginConfig.objects.all()
|
||||
serializer_class = PluginSerializers.PluginAdminDetailSerializer
|
||||
permission_classes = [InvenTree.permissions.IsAdminOrAdminScope]
|
||||
lookup_field = 'key'
|
||||
lookup_url_kwarg = 'plugin'
|
||||
|
||||
@@ -292,7 +296,10 @@ class PluginSettingList(ListAPI):
|
||||
queryset = PluginSetting.objects.all()
|
||||
serializer_class = PluginSerializers.PluginSettingSerializer
|
||||
|
||||
permission_classes = [InvenTree.permissions.GlobalSettingsPermissions]
|
||||
permission_classes = [
|
||||
permissions.IsAuthenticated,
|
||||
InvenTree.permissions.GlobalSettingsPermissions,
|
||||
]
|
||||
|
||||
filter_backends = [DjangoFilterBackend]
|
||||
|
||||
@@ -365,7 +372,10 @@ class PluginAllSettingList(APIView):
|
||||
- GET: return all settings for a plugin config
|
||||
"""
|
||||
|
||||
permission_classes = [InvenTree.permissions.GlobalSettingsPermissions]
|
||||
permission_classes = [
|
||||
permissions.IsAuthenticated,
|
||||
InvenTree.permissions.GlobalSettingsPermissions,
|
||||
]
|
||||
|
||||
@extend_schema(
|
||||
responses={200: PluginSerializers.PluginSettingSerializer(many=True)}
|
||||
@@ -393,6 +403,11 @@ class PluginSettingDetail(RetrieveUpdateAPI):
|
||||
queryset = PluginSetting.objects.all()
|
||||
serializer_class = PluginSerializers.PluginSettingSerializer
|
||||
|
||||
permission_classes = [
|
||||
permissions.IsAuthenticated,
|
||||
InvenTree.permissions.GlobalSettingsPermissions,
|
||||
]
|
||||
|
||||
def get_object(self):
|
||||
"""Lookup the plugin setting object, based on the URL.
|
||||
|
||||
@@ -415,9 +430,6 @@ class PluginSettingDetail(RetrieveUpdateAPI):
|
||||
setting_key, plugin=plugin.plugin_config()
|
||||
)
|
||||
|
||||
# Staff permission required
|
||||
permission_classes = [InvenTree.permissions.GlobalSettingsPermissions]
|
||||
|
||||
|
||||
class PluginUserSettingList(APIView):
|
||||
"""List endpoint for all user settings for a specific plugin.
|
||||
|
||||
@@ -816,3 +816,48 @@ class PluginLockedSettingsTest(PluginMixin, InvenTreeAPITestCase):
|
||||
self.assertFalse(
|
||||
response.data['read_only'], msg=f'{key} should not be read_only'
|
||||
)
|
||||
|
||||
|
||||
class PluginUnauthenticatedAccessTest(PluginMixin, InvenTreeAPITestCase):
|
||||
"""Ensure plugin API endpoints reject unauthenticated requests.
|
||||
|
||||
Tests the four endpoints hardened on the permissions-fix branch:
|
||||
- PluginDetail (api-plugin-detail)
|
||||
- PluginSettingList (api-plugin-setting-list)
|
||||
- PluginAllSettingList (api-plugin-settings)
|
||||
- PluginSettingDetail (api-plugin-setting-detail)
|
||||
"""
|
||||
|
||||
superuser = True
|
||||
PLUGIN_SLUG = 'sample'
|
||||
SETTING_KEY = 'API_KEY'
|
||||
|
||||
def setUp(self):
|
||||
"""Activate sample plugin, then log out to simulate an anonymous client."""
|
||||
super().setUp()
|
||||
from plugin.registry import registry
|
||||
|
||||
registry.set_plugin_state(self.PLUGIN_SLUG, True)
|
||||
self.client.logout()
|
||||
|
||||
def test_plugin_detail_unauthenticated(self):
|
||||
"""GET /api/plugins/<slug>/ must return 401 for unauthenticated users."""
|
||||
url = reverse('api-plugin-detail', kwargs={'plugin': self.PLUGIN_SLUG})
|
||||
self.get(url, expected_code=401)
|
||||
|
||||
def test_plugin_setting_list_unauthenticated(self):
|
||||
"""GET /api/plugins/settings/ must return 401 for unauthenticated users."""
|
||||
self.get(reverse('api-plugin-setting-list'), expected_code=401)
|
||||
|
||||
def test_plugin_all_settings_unauthenticated(self):
|
||||
"""GET /api/plugins/<slug>/settings/ must return 401 for unauthenticated users."""
|
||||
url = reverse('api-plugin-settings', kwargs={'plugin': self.PLUGIN_SLUG})
|
||||
self.get(url, expected_code=401)
|
||||
|
||||
def test_plugin_setting_detail_unauthenticated(self):
|
||||
"""GET /api/plugins/<slug>/settings/<key>/ must return 401 for unauthenticated users."""
|
||||
url = reverse(
|
||||
'api-plugin-setting-detail',
|
||||
kwargs={'plugin': self.PLUGIN_SLUG, 'key': self.SETTING_KEY},
|
||||
)
|
||||
self.get(url, expected_code=401)
|
||||
|
||||
Reference in New Issue
Block a user