Skip to content

Commit

Permalink
Release 5.8.19
Browse files Browse the repository at this point in the history
### Changelog:
* Fix(backend): Fixed permissions in schema generations for nested views.

See merge request vst/vst-utils!622
  • Loading branch information
onegreyonewhite committed Dec 21, 2023
2 parents 8817be5 + d51d445 commit 6b6e372
Show file tree
Hide file tree
Showing 11 changed files with 332 additions and 86 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Generated by Django 4.2.8 on 2023-12-20 06:45

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('test_proj', '0042_testexternalcustommodel'),
]

operations = [
migrations.CreateModel(
name='Manufacturer',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
],
options={
'default_related_name': 'manufacturers',
},
),
migrations.CreateModel(
name='Store',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
],
options={
'default_related_name': 'stores',
},
),
migrations.CreateModel(
name='Product',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('price', models.DecimalField(decimal_places=2, max_digits=10)),
('manufacturer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='test_proj.manufacturer')),
('store', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='test_proj.store')),
],
options={
'default_related_name': 'products',
},
),
migrations.CreateModel(
name='Option',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='test_proj.product')),
],
options={
'default_related_name': 'options',
},
),
migrations.AddField(
model_name='manufacturer',
name='store',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='test_proj.store'),
),
migrations.CreateModel(
name='Attribute',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='test_proj.product')),
],
options={
'default_related_name': 'attributes',
},
),
]
1 change: 1 addition & 0 deletions test_src/test_proj/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@
from .fields_testing import Post, ExtraPost, Author, ModelWithChangedFk, ModelWithCrontabField, ModelWithUuidFK, ModelWithUuidPk
from .cacheable import CachableModel, CachableProxyModel
from .deep import Group, ModelWithNestedModels, GroupWithFK, AnotherDeepNested, ProtectedBySignal
from .nested_models import Option, Attribute, Store, Product, Manufacturer
2 changes: 1 addition & 1 deletion test_src/test_proj/models/deep.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,5 +66,5 @@ class Meta:
'protected': {
'allow_append': True,
'model': ProtectedBySignal,
}
},
}
84 changes: 84 additions & 0 deletions test_src/test_proj/models/nested_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
from django.db import models
from django.dispatch import receiver
from django.db.models.signals import pre_delete
from django.core.validators import ValidationError
from rest_framework.permissions import BasePermission
from rest_framework.request import Request
from rest_framework.views import APIView

from vstutils.models import BaseModel


class DisallowStaffPermission(BasePermission):
def has_permission(self, request, view):
if not request.user.is_superuser and request.user.is_staff:
return False
return super().has_permission(request, view)


class Option(BaseModel):
name = models.CharField(max_length=255)
product = models.ForeignKey('Product', on_delete=models.CASCADE)

class Meta:
default_related_name = 'options'


class Attribute(BaseModel):
name = models.CharField(max_length=255)
product = models.ForeignKey('Product', on_delete=models.CASCADE)

class Meta:
default_related_name = 'attributes'
_permission_classes = [DisallowStaffPermission]


class Product(BaseModel):
name = models.CharField(max_length=255)
price = models.DecimalField(max_digits=10, decimal_places=2)
store = models.ForeignKey('Store', on_delete=models.CASCADE)
manufacturer = models.ForeignKey('Manufacturer', on_delete=models.CASCADE)

class Meta:
default_related_name = 'products'
_nested = {
'options': {
'allow_append': True,
'model': Option,
},
'attributes': {
'allow_append': True,
'model': Attribute,
}
}


class Manufacturer(BaseModel):
name = models.CharField(max_length=255)
store = models.ForeignKey('Store', on_delete=models.CASCADE)

class Meta:
default_related_name = 'manufacturers'
_nested = {
'products': {
'allow_append': False,
'model': Product,
}
}


class Store(BaseModel):
name = models.CharField(max_length=255)

class Meta:
default_related_name = 'stores'
_nested = {
'products': {
'allow_append': True,
'model': Product,
},
'manufacturers': {
'allow_append': False,
'model': Manufacturer,
}
}
3 changes: 3 additions & 0 deletions test_src/test_proj/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@
API[VST_API_VERSION][r'modelwithnested'] = dict(
model='test_proj.models.ModelWithNestedModels'
)
API[VST_API_VERSION][r'stores'] = dict(
model='test_proj.models.Store'
)
API[VST_API_VERSION][r'modelwithcrontab'] = dict(
model='test_proj.models.ModelWithCrontabField'
)
Expand Down
73 changes: 73 additions & 0 deletions test_src/test_proj/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@
ModelWithNestedModels,
ProtectedBySignal,
ModelWithUuidPk,
Store,
Manufacturer,
Option,
Attribute,
Product
)
from rest_framework.exceptions import ValidationError
from base64 import b64encode
Expand Down Expand Up @@ -2321,6 +2326,23 @@ def has_deep_parent_filter(params):
schema = self.endpoint_schema()
self.assertTrue(schema['definitions']['User']['properties']['is_staff']['readOnly'])

# check that nested endponit's permissions took into account
user = self._create_user(is_super_user=False, is_staff=True)
with self.user_as(self, user):
schema = self.endpoint_schema()
schemas_differance = set(api['paths'].keys()) - set(schema['paths'].keys())
expected_differance = {
'/stores/{id}/products/{products_id}/attributes/',
'/stores/{id}/products/{products_id}/attributes/{attributes_id}/',
'/stores/{id}/manufacturers/{manufacturers_id}/products/{products_id}/attributes/{attributes_id}/',
'/stores/{id}/manufacturers/{manufacturers_id}/products/{products_id}/attributes/',
}
# Check that only expected endpoints were banned.
self.assertEqual(
schemas_differance,
expected_differance
)

def test_search_fields(self):
self.assertEqual(
self.get_model_class('test_proj.Variable').generated_view.search_fields,
Expand Down Expand Up @@ -5043,6 +5065,57 @@ def serializer_test(serializer):
generated_serializer = ModelWithBinaryFiles.generated_view.serializer_class()
serializer_test(generated_serializer)

def test_nested_views_permissions(self):
# Test nested model viewsets permissions.
store = Store.objects.create(
name='test'
)
manufacturer = Manufacturer.objects.create(
name='test man',
store=store
)
product = Product.objects.create(
name='test prod',
store=store,
price = 100,
manufacturer=manufacturer
)
attr = Attribute.objects.create(
name='test attr',
product=product
)
option = Option.objects.create(
name='test option',
product=product,
)

endpoints_to_test = [
{'method': 'get', 'path': f'/stores/{store.id}/products/{product.id}/attributes/'},
{'method': 'get', 'path': f'/stores/{store.id}/products/{product.id}/attributes/{attr.id}/'},
{'method': 'get', 'path': f'/stores/{store.id}/manufacturers/{manufacturer.id}/products/{product.id}/attributes/{attr.id}/'},
{'method': 'get', 'path': f'/stores/{store.id}/manufacturers/{manufacturer.id}/products/{product.id}/attributes/'},
]

always_available = [
{'method': 'get', 'path': f'/stores/{store.id}/products/{product.id}/options/{option.id}/'},
{'method': 'get', 'path': f'/stores/{store.id}/products/{product.id}/options/'},
]

results = self.bulk(endpoints_to_test + always_available)

for result in results:
self.assertEqual(result['status'], 200, result['path'])

user = self._create_user(is_super_user=False, is_staff=True)
with self.user_as(self, user):
results = self.bulk(endpoints_to_test)
for result in results:
self.assertEqual(result['status'], 403, result['path'])

results = self.bulk(always_available)
for result in results:
self.assertEqual(result['status'], 200, result['path'])

def test_deep_nested(self):
results = self.bulk([
# [0-2] Create 3 nested objects
Expand Down
2 changes: 1 addition & 1 deletion vstutils/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# pylint: disable=django-not-available
__version__: str = '5.8.18'
__version__: str = '5.8.19'
18 changes: 9 additions & 9 deletions vstutils/api/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@
from ..utils import raise_context


class IsAuthenticatedOpenApiRequest(permissions.IsAuthenticated):
def is_openapi_request(request):
return (
request.path.startswith(f'/{settings.API_URL}/openapi/') or
request.path.startswith(f'/{settings.API_URL}/endpoint/') or
request.path == f'/{settings.API_URL}/{request.version}/_openapi/'
)

def is_openapi(self, request):
return (
request.path.startswith(f'/{settings.API_URL}/openapi/') or
request.path.startswith(f'/{settings.API_URL}/endpoint/') or
request.path == f'/{settings.API_URL}/{request.version}/_openapi/'
)

class IsAuthenticatedOpenApiRequest(permissions.IsAuthenticated):
def has_permission(self, request, view):
return self.is_openapi(request) or super().has_permission(request, view)
return is_openapi_request(request) or super().has_permission(request, view)


class SuperUserPermission(IsAuthenticatedOpenApiRequest):
Expand All @@ -29,7 +29,7 @@ def has_permission(self, request, view):
issubclass(view.get_queryset().model, AbstractUser) and
str(view.kwargs['pk']) in (str(request.user.pk), 'profile')
)
return self.is_openapi(request)
return is_openapi_request(request)

def has_object_permission(self, request, view, obj):
if request.user.is_superuser:
Expand Down
8 changes: 8 additions & 0 deletions vstutils/api/schema/generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
from drf_yasg.inspectors import field as field_insp
from vstutils.utils import raise_context_decorator_with_default

from .schema import get_nested_view_obj, _get_nested_view_and_subaction


def get_centrifugo_public_address(request: drf_request.Request):
address = settings.CENTRIFUGO_PUBLIC_HOST
Expand Down Expand Up @@ -103,6 +105,12 @@ def get_path_parameters(self, path, view_cls):
continue # nocv
return parameters

def should_include_endpoint(self, path, method, view, public):
nested_view, sub_action = _get_nested_view_and_subaction(view)
if nested_view and sub_action:
view = get_nested_view_obj(view, nested_view, sub_action, method)
return super().should_include_endpoint(path, method, view, public)

def get_operation_keys(self, subpath, method, view):
keys = super().get_operation_keys(subpath, method, view)
subpath_keys = list(filter(bool, subpath.split('/')))
Expand Down
Loading

0 comments on commit 6b6e372

Please sign in to comment.