Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ jobs:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}

# Caches downloaded .deb packages to speed up future installations
- name: Cache APT packages
uses: actions/cache@v4
Expand All @@ -51,7 +51,7 @@ jobs:
restore-keys: |
apt-${{ runner.os }}-

# Disables man-db auto-update to prevent delays during package installation
# Disables man-db auto-update to prevent delays during package installation
- name: Disable man page auto-update
run: |
echo 'set man-db/auto-update false' | sudo debconf-communicate >/dev/null
Expand All @@ -71,6 +71,7 @@ jobs:
pip install -U pip wheel setuptools
pip install -U -r requirements-test.txt
pip install -U -e .
pip install -UI "openwisp-users @ https://github.com/openwisp/openwisp-users/tarball/issues/238-view-shared-objects" "cryptography~=43.0.3"
pip install ${{ matrix.django-version }}
sudo npm install -g prettier

Expand Down
2 changes: 2 additions & 0 deletions openwisp_ipam/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ class IpAddressListCreateView(IpAddressOrgMixin, ProtectedAPIMixin, ListCreateAP
subnet_model = Subnet
serializer_class = IpAddressSerializer
pagination_class = ListViewPagination
organization_field = "subnet__organization"
organization_lookup = "organization__in"

def get_queryset(self):
subnet = get_object_or_404(self.subnet_model, pk=self.kwargs["subnet_id"])
Expand Down
3 changes: 3 additions & 0 deletions openwisp_ipam/static/openwisp-ipam/css/admin.css
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ section.subnet-visual {
background: rgba(149, 10, 10, 1);
border: 1px solid rgba(0, 0, 0, 0.4);
}
.subnet-visual a.disabled {
cursor: not-allowed;
}

.subnet-visual .page {
display: inline;
Expand Down
29 changes: 15 additions & 14 deletions openwisp_ipam/static/openwisp-ipam/js/subnet.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,20 +71,21 @@ function initHostsInfiniteScroll(
"</a>"
);
}
return (
'<a href=\"' +
address_add_url +
"?_to_field=id&amp;_popup=1&amp;ip_address=" +
addr.address +
"&amp;subnet=" +
current_subnet +
'"onclick="return showAddAnotherPopup(this);" ' +
'id="addr_' +
id +
'">' +
addr.address +
"</a>"
);
var anchorAttributes = 'class="disabled"';
if (hasSubnetChangePermission === "true") {
anchorAttributes =
'href=\"' +
address_add_url +
"?_to_field=id&amp;_popup=1&amp;ip_address=" +
addr.address +
"&amp;subnet=" +
current_subnet +
'"onclick="return showAddAnotherPopup(this);" ' +
'id="addr_' +
id +
'"';
}
return "<a " + anchorAttributes + " >" + addr.address + "</a>";
}
function pageContainer(page) {
var div = $('<div class="page"></div>');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ <h3 class="subnet-visual">{% trans 'Subnet Visual Display' %}</h3>
var current_subnet = '{{ original.pk }}';
var ipAddUrl = '{% url ipaddress_add_url %}'
var ipChangeUrl = '{% url ipaddress_change_url "1234" %}'
var hasSubnetChangePermission = {{ has_change_permission|yesno:"true,false" }};
django.jQuery(document).ready(function () {
initHostsInfiniteScroll(django.jQuery, current_subnet, ipAddUrl, ipChangeUrl, {{ ip_uuid| safe}})
});
Expand Down
77 changes: 76 additions & 1 deletion openwisp_ipam/tests/test_admin.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import json

import django
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import TestCase
from django.urls import reverse
from openwisp_users.tests.utils import TestMultitenantAdminMixin
from swapper import load_model

from . import CreateModelsMixin, PostDataMixin
Expand All @@ -14,7 +16,7 @@
IpAddress = load_model("openwisp_ipam", "IpAddress")


class TestAdmin(CreateModelsMixin, PostDataMixin, TestCase):
class TestAdmin(TestMultitenantAdminMixin, CreateModelsMixin, PostDataMixin, TestCase):
app_label = "openwisp_ipam"

def setUp(self):
Expand Down Expand Up @@ -438,3 +440,76 @@ def assert_response(response):
reverse("admin:ipam_export_subnet", args=[subnet.id]), follow=True
)
assert_response(response)

def test_superuser_create_shared_subnet(self):
admin = self._get_admin()
self.client.force_login(admin)
response = self.client.post(
reverse(f"admin:{self.app_label}_subnet_add"),
data={
"name": "test-subnet",
"subnet": "10.0.0.0/24",
"organization": "",
},
follow=True,
)
self.assertEqual(response.status_code, 200)
self.assertEqual(Subnet.objects.count(), 1)

def test_org_admin_view_shared_subnet(self):
subnet = self._create_subnet(organization=None, subnet="10.8.0.0/24")
self._test_org_admin_view_shareable_object(
reverse(f"admin:{self.app_label}_subnet_change", args=(subnet.id,)),
)

def test_org_admin_create_shared_subnet(self):
self._test_org_admin_create_shareable_object(
reverse(f"admin:{self.app_label}_subnet_add"),
payload={
"name": "test-subnet",
"subnet": "10.0.0.0/24",
"organization": "",
},
model=Subnet,
)

def test_superuser_create_shared_ip(self):
admin = self._get_admin()
self.client.force_login(admin)
shared_subnet = self._create_subnet(subnet="10.0.0.0/24", organization=None)
response = self.client.post(
reverse(f"admin:{self.app_label}_ipaddress_add"),
data={
"subnet": str(shared_subnet.id),
},
follow=True,
)
self.assertEqual(response.status_code, 200)
self.assertEqual(Subnet.objects.count(), 1)

def test_org_admin_view_shared_ip(self):
shared_subnet = self._create_subnet(subnet="10.0.0.0/24", organization=None)
ip = shared_subnet.request_ip()
self._test_org_admin_view_shareable_object(
reverse(f"admin:{self.app_label}_ipaddress_change", args=(ip.id,)),
expected_element=(
'<div class="form-row field-ip_address">\n\n\n<div>\n\n'
'<div class="flex-container">\n\n'
"<label>Ip address:</label>\n\n"
'<div class="readonly">10.0.0.1</div>\n\n\n'
"</div>\n\n</div>\n\n\n</div>"
),
)

def test_org_admin_create_shared_ip(self):
shared_subnet = self._create_subnet(subnet="10.0.0.0/24", organization=None)
self._test_org_admin_create_shareable_object(
reverse(f"admin:{self.app_label}_ipaddress_add"),
payload={
"subnet": str(shared_subnet.id),
},
model=IpAddress,
error_message=(
'<ul class="errorlist"{}><li>This field is required.</li></ul>'
).format(' id="id_ip_address_error"' if django.VERSION >= (5, 2) else ""),
)
85 changes: 82 additions & 3 deletions openwisp_ipam/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@

from django.contrib.auth import get_user_model
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import TestCase
from django.urls import reverse
from openwisp_users.tests.utils import TestMultitenantAdminMixin
from openwisp_users.tests.test_api import APITestCase
from swapper import load_model

from . import CreateModelsMixin, PostDataMixin
Expand All @@ -14,7 +13,7 @@
IpAddress = load_model("openwisp_ipam", "IpAddress")


class TestApi(TestMultitenantAdminMixin, CreateModelsMixin, PostDataMixin, TestCase):
class TestApi(CreateModelsMixin, PostDataMixin, APITestCase):
def setUp(self):
super().setUp()
self._login()
Expand Down Expand Up @@ -351,3 +350,83 @@ def test_subnet_single_hosts_first_address(self):
host_address_32 = response.data["results"][0]["address"]
self.assertEqual(host_address_128, "2001:db00::")
self.assertEqual(host_address_32, "192.168.0.0")

def test_superuser_access_shared_subnet(self):
self._logout()
self._test_superuser_access_shared_object(
token=None,
listview_name="ipam:subnet_list_create",
detailview_name="ipam:subnet",
create_payload={
"name": "test-subnet",
"subnet": "10.0.0.0/24",
"description": "Test Subnet",
"organization": None,
},
update_payload={
"name": "updated-subnet",
"subnet": "10.0.0.0/24",
},
expected_count=1,
)

def test_org_manager_access_shared_subnet(self):
self._logout()
shared_subnet = self._create_subnet(organization=None, subnet="10.0.0.0/24")
self._test_org_user_access_shared_object(
listview_path=reverse("ipam:subnet_list_create"),
detailview_path=reverse("ipam:subnet", args=[shared_subnet.pk]),
create_payload={
"name": "test-subnet",
"subnet": "10.0.0.0/24",
"description": "Test Subnet",
"organization": None,
},
update_payload={
"name": "updated-subnet",
"subnet": "10.0.0.0/24",
},
expected_count=1,
)

def test_superuser_access_shared_ip(self):
self._logout()
subnet = self._create_subnet(subnet="10.0.0.0/24", organization=None)
self._test_superuser_access_shared_object(
token=None,
listview_path=reverse("ipam:list_create_ip_address", args=[subnet.id]),
detailview_name="ipam:ip_address",
create_payload={
"ip_address": "10.0.0.1",
"subnet": str(subnet.id),
"description": "Test IP",
},
update_payload={
"description": "updated-ip",
"ip_address": "10.0.0.1",
"subnet": str(subnet.id),
},
expected_count=1,
)

def test_org_manager_access_shared_ip(self):
self._logout()
shared_subnet = self._create_subnet(subnet="10.0.0.0/24", organization=None)
shared_ip = shared_subnet.request_ip()
self._test_org_user_access_shared_object(
listview_path=reverse(
"ipam:list_create_ip_address", args=[shared_subnet.id]
),
detailview_path=reverse("ipam:ip_address", args=[shared_ip.id]),
create_payload={
"ip_address": "10.0.0.2",
"subnet": str(shared_subnet.id),
"description": "Test IP",
},
update_payload={
"description": "updated-ip",
"ip_address": "10.0.0.1",
"subnet": str(shared_subnet.id),
},
expected_count=1,
)
Loading