-
-
Notifications
You must be signed in to change notification settings - Fork 225
[change] Make batch user creation organization field readonly #618
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?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -515,6 +515,54 @@ class Meta: | |||||||||||||||||||||||||||||||||||||
| read_only_fields = ("status", "user_credentials", "created", "modified") | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| class RadiusBatchUpdateSerializer(RadiusBatchSerializer): | ||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||
| Serializer for updating RadiusBatch objects. | ||||||||||||||||||||||||||||||||||||||
| Makes the organization field readonly for existing objects. | ||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| organization_slug = RadiusOrganizationField( | ||||||||||||||||||||||||||||||||||||||
| help_text=("Slug of the organization for creating radius batch."), | ||||||||||||||||||||||||||||||||||||||
| required=False, | ||||||||||||||||||||||||||||||||||||||
| label="organization", | ||||||||||||||||||||||||||||||||||||||
| slug_field="slug", | ||||||||||||||||||||||||||||||||||||||
| write_only=True, | ||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| def validate(self, data): | ||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||
| Validates partial updates while preserving model-level validation. | ||||||||||||||||||||||||||||||||||||||
| Ignores organization_slug if provided since organization is readonly. | ||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||
| data.pop("organization_slug", None) | ||||||||||||||||||||||||||||||||||||||
| strategy = data.get("strategy") or (self.instance and self.instance.strategy) | ||||||||||||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||||||||||||
| strategy == "prefix" | ||||||||||||||||||||||||||||||||||||||
| and "number_of_users" in data | ||||||||||||||||||||||||||||||||||||||
| and not data.get("number_of_users") | ||||||||||||||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||||||||||||||
| raise serializers.ValidationError( | ||||||||||||||||||||||||||||||||||||||
| {"number_of_users": _("The field number_of_users cannot be empty")} | ||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||
| validated_data = serializers.ModelSerializer.validate(self, data) | ||||||||||||||||||||||||||||||||||||||
| # Run model-level validation (full_clean) for update | ||||||||||||||||||||||||||||||||||||||
| if self.instance: | ||||||||||||||||||||||||||||||||||||||
| batch_data = { | ||||||||||||||||||||||||||||||||||||||
| field: validated_data.get(field, getattr(self.instance, field)) | ||||||||||||||||||||||||||||||||||||||
| for field in validated_data | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| batch_data.pop("number_of_users", None) | ||||||||||||||||||||||||||||||||||||||
| for field, value in batch_data.items(): | ||||||||||||||||||||||||||||||||||||||
| setattr(self.instance, field, value) | ||||||||||||||||||||||||||||||||||||||
| self.instance.full_clean() | ||||||||||||||||||||||||||||||||||||||
| return validated_data | ||||||||||||||||||||||||||||||||||||||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| class Meta: | ||||||||||||||||||||||||||||||||||||||
| model = RadiusBatch | ||||||||||||||||||||||||||||||||||||||
| fields = "__all__" | ||||||||||||||||||||||||||||||||||||||
| read_only_fields = ("created", "modified", "user_credentials", "organization") | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+560
to
+563
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Check admin readonly fields vs serializer readonly fields
echo "=== Admin readonly fields for non-pending batches ==="
rg -n "strategy|organization|prefix|csvfile|number_of_users|users|expiration_date|name|status" openwisp_radius/admin.py | grep -A2 -B2 "get_readonly"
echo ""
echo "=== Serializer read_only_fields ==="
rg -n "read_only_fields" openwisp_radius/api/serializers.pyRepository: openwisp/openwisp-radius Length of output: 548 🏁 Script executed: #!/bin/bash
# Search for RadiusBatchAdmin and get_readonly_fields
echo "=== Search for RadiusBatchAdmin class ==="
rg -A 30 "class RadiusBatchAdmin" openwisp_radius/admin.py
echo ""
echo "=== Search for get_readonly_fields method ==="
rg -B 5 -A 20 "def get_readonly_fields" openwisp_radius/admin.py | head -100Repository: openwisp/openwisp-radius Length of output: 1837 🏁 Script executed: #!/bin/bash
# Find RadiusBatchUpdateSerializer and check for status handling
echo "=== RadiusBatchUpdateSerializer full implementation ==="
rg -B 50 "class RadiusBatchUpdateSerializer" openwisp_radius/api/serializers.py | tail -60
echo ""
echo "=== Check validate method for status handling ==="
rg -A 40 "def validate" openwisp_radius/api/serializers.py | grep -A 40 "class RadiusBatchUpdateSerializer" | tail -45Repository: openwisp/openwisp-radius Length of output: 3563 🏁 Script executed: #!/bin/bash
# Check RadiusBatch model for clean() method
echo "=== RadiusBatch model clean() method ==="
rg -B 5 -A 30 "def clean" openwisp_radius/models.py | grep -A 35 "class RadiusBatch" | tail -50
echo ""
echo "=== Check RadiusBatch status field and related logic ==="
rg -B 3 -A 8 "status.*=" openwisp_radius/models.py | grep -A 10 -B 3 "RadiusBatch\|pending\|COMPLETED" | head -60Repository: openwisp/openwisp-radius Length of output: 166 🏁 Script executed: #!/bin/bash
# Find RadiusBatch model
echo "=== Find RadiusBatch model ==="
fd "models.py" openwisp_radius/ | head -5
echo ""
echo "=== Look for RadiusBatch class definition ==="
rg -A 50 "class RadiusBatch" openwisp_radius/models.py | head -80
echo ""
echo "=== Check if there's a clean method anywhere in the file ==="
rg "def clean" openwisp_radius/models.pyRepository: openwisp/openwisp-radius Length of output: 1272 🏁 Script executed: #!/bin/bash
# Find AbstractRadiusBatch and check for clean() method
echo "=== Look for AbstractRadiusBatch class ==="
rg -B 5 -A 100 "class AbstractRadiusBatch" openwisp_radius/base/models.py | head -150
echo ""
echo "=== Check for clean method ==="
rg -B 3 -A 20 "def clean" openwisp_radius/base/models.py | grep -A 22 -B 3 "RadiusBatch"Repository: openwisp/openwisp-radius Length of output: 3419 🏁 Script executed: #!/bin/bash
# Get the full clean method from AbstractRadiusBatch
rg -A 40 "def clean\(self\):" openwisp_radius/base/models.pyRepository: openwisp/openwisp-radius Length of output: 9623
The admin's This creates an inconsistency: the admin prevents editing these fields for completed batches, but the API allows it. Consider making these fields conditionally readonly based on batch status to match admin behavior, or document this difference explicitly. 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+560
to
+564
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Cross-check serializer mutability vs documented read-only contract
rg -n -C3 "class RadiusBatchUpdateSerializer|read_only_fields" openwisp_radius/api/serializers.py
rg -n -C3 "Parameters for \\*\\*PUT\\*\\*/\\*\\*PATCH\\*\\*|Fields that are \\*\\*read-only\\*\\*" docs/user/rest-api.rstRepository: openwisp/openwisp-radius Length of output: 2179 🏁 Script executed: # Get full context of RadiusBatch API documentation
sed -n '880,920p' docs/user/rest-api.rstRepository: openwisp/openwisp-radius Length of output: 1424 🏁 Script executed: # Check RadiusBatch model definition and fields
rg -n "class RadiusBatch" openwisp_radius/models.py -A 50Repository: openwisp/openwisp-radius Length of output: 1129 🏁 Script executed: # Verify the parent RadiusBatchSerializer definition to understand inheritance
sed -n '512,520p' openwisp_radius/api/serializers.pyRepository: openwisp/openwisp-radius Length of output: 336 RadiusBatchUpdateSerializer's The serializer declares only ("created", "modified", "user_credentials", "organization") as read-only, but the API documentation specifies that Suggested fix class Meta:
model = RadiusBatch
fields = "__all__"
- read_only_fields = ("created", "modified", "user_credentials", "organization")
+ read_only_fields = (
+ "created",
+ "modified",
+ "user_credentials",
+ "organization",
+ "strategy",
+ "csvfile",
+ "prefix",
+ "users",
+ "status",
+ )📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| class PasswordResetSerializer(BasePasswordResetSerializer): | ||||||||||||||||||||||||||||||||||||||
| input = serializers.CharField() | ||||||||||||||||||||||||||||||||||||||
| email = None | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -1556,6 +1556,112 @@ def test_radius_user_group_serializer_without_view_context(self): | |||||||||||
| self.assertEqual(serializer._user, None) | ||||||||||||
| self.assertEqual(serializer.fields["group"].queryset.count(), 0) | ||||||||||||
|
|
||||||||||||
| def _get_admin_auth_header(self): | ||||||||||||
| """Helper method to get admin authentication header""" | ||||||||||||
| login_payload = {"username": "admin", "password": "tester"} | ||||||||||||
| login_url = reverse("radius:user_auth_token", args=[self.default_org.slug]) | ||||||||||||
| login_response = self.client.post(login_url, data=login_payload) | ||||||||||||
| self.assertEqual(login_response.status_code, status.HTTP_200_OK) | ||||||||||||
| return f"Bearer {login_response.json()['key']}" | ||||||||||||
|
||||||||||||
| return f"Bearer {login_response.json()['key']}" | |
| self.assertEqual(login_response.status_code, status.HTTP_200_OK) | |
| login_response_data = login_response.json() | |
| self.assertIn("key", login_response_data) | |
| return f"Bearer {login_response_data['key']}" |
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.
🧹 Nitpick | 🔵 Trivial
Potential issue: _get_admin() not called before authentication.
The helper method authenticates as "admin"/"tester" but doesn't call self._get_admin() to ensure the admin user exists first. This works in the current tests because they all call _radius_batch_post_request() before _get_admin_auth_header(), which internally creates the admin user. However, if this helper is used in future tests without that precondition, it will fail with a 400 response.
Consider making the helper self-contained:
💡 Suggested improvement
def _get_admin_auth_header(self):
"""Helper method to get admin authentication header"""
+ self._get_admin() # Ensure admin user exists
login_payload = {"username": "admin", "password": "tester"}
login_url = reverse("radius:user_auth_token", args=[self.default_org.slug])
login_response = self.client.post(login_url, data=login_payload)
self.assertEqual(login_response.status_code, status.HTTP_200_OK)
return f"Bearer {login_response.json()['key']}"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@openwisp_radius/tests/test_api/test_api.py` around lines 1559 - 1565, The
_get_admin_auth_header helper assumes an admin exists; make it self-contained by
ensuring the admin user is created before attempting login: call
self._get_admin() (or otherwise create the admin) at the start of
_get_admin_auth_header so the POST to reverse("radius:user_auth_token",
args=[self.default_org.slug]) never fails with 400; keep the rest of the logic
(login_payload, client.post, status check, return token) intact and do not rely
on _radius_batch_post_request being called by callers.
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.
🧹 Nitpick | 🔵 Trivial
Consider adding cross-organization permission test.
The permission tests cover unauthenticated, unprivileged user, and admin access. Consider adding a test case for an organization manager who has permissions but tries to update a batch belonging to a different organization they don't manage - this would verify the queryset filtering in BatchUpdateView.get_queryset().
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@openwisp_radius/tests/test_api/test_api.py` around lines 1631 - 1663, Add a
cross-organization case to test_batch_update_permissions: create a second
Organization and a RadiusBatch that belongs to that other organization, then
authenticate as an organization manager who has permissions only for the first
org (use whatever helper you have for manager auth, e.g.
_get_org_manager_auth_header or create a manager user and Token) and attempt to
PATCH the other-org batch URL; assert the response is 403 to validate
BatchUpdateView.get_queryset() filtering. Ensure you reference the existing test
method test_batch_update_permissions, the RadiusBatch instance for the other
organization, and BatchUpdateView.get_queryset() behavior when adding this
assertion.
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.
🧹 Nitpick | 🔵 Trivial
Documentation could be more specific about updatable fields.
The documentation states that
organizationis read-only but implies "all other editable fields" can be modified. However, per the admin implementation and likely serializer constraints, fields likestrategy,csvfile,prefix,users, andstatusmay also be read-only after creation (especially for non-pending batches). Consider adding a parameter table for PUT/PATCH similar to the POST section, clarifying which fields are actually updatable.🤖 Prompt for AI Agents