-
-
Notifications
You must be signed in to change notification settings - Fork 70
[change] Improve endpoints to download firmware images #306
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks on the correct track. Check my suggestion below.
@@ -970,9 +985,11 @@ def test_firmware_download(self): | |||
url = reverse('upgrader:api_firmware_download', args=[image.build.pk, image.pk]) | |||
with self.subTest("Test as operator"): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to add a sub-test to also test as an administrator.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we are testing it for admin too below
) | ||
|
||
# Create a custom permission group without view permission | ||
from django.contrib.auth.models import Group |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any reason why this import isn't at the top of the file?
Load the Group model using the swapper module
import swapper
Group = swapper.load_model('openwisp_users', 'Group')
if not user.is_superuser: | ||
perm_codename = get_permission_codename('view', self.model._meta) | ||
view_perm = f'{self.model._meta.app_label}.{perm_codename}' | ||
has_view_perm = user.has_perm(view_perm, self.object) | ||
if not has_view_perm: | ||
return False |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you consider using the PermissionRequiredMixin instead of performing these checks manually?
def to_representation(self, instance): | ||
ret = super().to_representation(instance) | ||
# Replace 'file' with the URL from file_url | ||
if 'file_url' in ret: | ||
ret['file'] = ret.pop('file_url') | ||
return ret |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we are already overriding the value of field
value in the to_representation, then why do we need to add another field file_url
to the serializer? We can directly call the get_file_url
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not convinced with the changes to the tests. Can you clarify my doubts?
@@ -45,10 +46,26 @@ class Meta: | |||
|
|||
|
|||
class FirmwareImageSerializer(BaseSerializer): | |||
file = serializers.FileField(required=False) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you explain why defining this field is requried? I thought, it would be added automatically by ModelSerializer.
elif hasattr(instance, 'file'): | ||
ret['file'] = instance.file.url |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't this the default behaviour? If we leave the 'file' field unchanged in the code, then it will contain the file.url
.
I tested this locally with the following code:
In [1]: from openwisp_firmware_upgrader.api.serializers import FirmwareImage, FirmwareImageSerializer
...:
...: fw_image = FirmwareImage.objects.first()
...: fw_image.file.url
...:
...: serializer = FirmwareImageSerializer(instance=fw_image)
In [2]: serializer.data
Out[2]: {'id': '144d9284-6b80-471b-a2dd-52cbc3c18565', 'file': '/firmware/503ac721-94fd-469f-85a7-977330f50499/openwrt-23.05.0-ramips-mt7621-yuncore_ax820-squashfs-sysupgrade.bin', 'created': '2025-04-28T11:15:07.867479+02:00', 'modified': '2025-04-28T11:15:07.867479+02:00', 'type': 'ramips-mt7621-yuncore_ax820-squashfs-sysupgrade.bin', 'build': UUID('503ac721-94fd-469f-85a7-977330f50499')}
I think, this change makes the serializer inconsistent. We should try to return the URL to the firmware download API even if request is not present in the serializer. In order to remain consistent with DRF, we can return the relative path to the API firmware download view if there's no request object.
) | ||
has_view_perm = user.has_perm(perm, self.object) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't need to check this here as the PermissionRequiredMixin
should handle this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's also skip accessing the object here. We don't enforce object level permission checks in OpenWISP.
def handle_no_permission(self): | ||
""" | ||
Return empty response for unauthorized API requests | ||
""" | ||
if 'api' in self.request.path: | ||
return HttpResponse(status=403) | ||
return super().handle_no_permission() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I commented this code, and none of the tests fails. This is bad.
Is this code relevant? Won't raise_exception = True
cover this scenario?
url = reverse('upgrader:api_firmware_download', args=[image.build.pk, image.pk]) | ||
with self.subTest(url=url): | ||
with self.assertNumQueries(1): | ||
r = client.get(url) | ||
self.assertEqual(r.status_code, 401) | ||
self.assertEqual(r.status_code, 403) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is wrong. We should fix the view so it returns 401 instead of 403 for unauthenticated users.
@@ -68,9 +74,41 @@ def test_staff_operator_with_same_organization(self): | |||
user=staff_user, organization=self.test_org, is_admin=True | |||
) | |||
self.client.force_login(staff_user) | |||
self._download_firmware_assert_status(200) | |||
self._download_firmware_assert_status(403) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why did you change this? Aren't operators allowed to view the FirmwareImage?
openwisp-firmware-upgrader/openwisp_firmware_upgrader/migrations/__init__.py
Lines 28 to 34 in ee878d5
operators_read_only_admins_manage = [ | |
'build', | |
'devicefirmware', | |
'firmwareimage', | |
'batchupgradeoperation', | |
'upgradeoperation', | |
] |
|
||
def test_superuser(self): | ||
user = self._get_admin() | ||
self.client.force_login(user) | ||
self._download_firmware_assert_status(200) | ||
|
||
def test_view_permission_check(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you sure this test is correct? I believe the code comments are outdated.
A user being Organization Admin and a user having Administrator group are two different things. Is this clear?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes clear now
# Get object first since it's needed for organization check | ||
self.object = self.get_object() | ||
|
||
# Superusers can always access | ||
if user.is_superuser: | ||
return True | ||
|
||
return user.is_staff and user.is_manager( | ||
self.object.build.category.organization | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does the logic stays same as before?
# Add view permission first | ||
content_type = ContentType.objects.get_for_model(FirmwareImage) | ||
perm_codename = get_permission_codename('view', FirmwareImage._meta) | ||
view_perm = Permission.objects.get( | ||
content_type=content_type, codename=perm_codename | ||
) | ||
staff_user.user_permissions.add(view_perm) | ||
|
||
self._create_org_user(user=staff_user, organization=org, is_admin=True) | ||
self._download_firmware_assert_status(200) | ||
|
||
# Remove org manager status | ||
org_user = staff_user.openwisp_users_organization.get( | ||
organization_users__organization=org | ||
) | ||
org_user.is_admin = False | ||
org_user.save() | ||
|
||
# Remove staff status | ||
staff_user.is_staff = False | ||
staff_user.save() | ||
|
||
# Restore org manager status | ||
org_user.is_admin = True | ||
org_user.save() | ||
|
||
# No access (missing staff status) | ||
self._download_firmware_assert_status(403) | ||
|
||
# Remove view permission | ||
staff_user.user_permissions.remove(view_perm) | ||
|
||
# No access (missing view permission) | ||
self._download_firmware_assert_status(403) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good, can you refactor the code to use subTest
? It helps to organize code for testing different scenarios. You can look around the code for reference.
https://github.com/search?q=repo%3Aopenwisp%2Fopenwisp-firmware-upgrader+subTest&type=code
Checklist
Reference to Existing Issue
Closes #69
Please open a new issue if there isn't an existing issue yet.