Skip to content

Commit

Permalink
[api] Implement multitenancy in API #61
Browse files Browse the repository at this point in the history
Closes #61
  • Loading branch information
purhan committed Nov 14, 2020
1 parent a4f3c93 commit 8f36d31
Show file tree
Hide file tree
Showing 4 changed files with 269 additions and 11 deletions.
10 changes: 10 additions & 0 deletions openwisp_ipam/api/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from openwisp_users.api.permissions import IsOrganizationMember
from rest_framework.exceptions import PermissionDenied


def validate_permission(self, user, organization):
if not (
user.is_superuser
or IsOrganizationMember.validate_membership(self, user, organization)
):
raise PermissionDenied(IsOrganizationMember.message)
36 changes: 32 additions & 4 deletions openwisp_ipam/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
from django.http import HttpResponse
from django.utils.translation import gettext_lazy as _
from openwisp_users.api.authentication import BearerAuthentication
from openwisp_users.api.permissions import IsOrganizationMember
from rest_framework import pagination, serializers, status
from rest_framework.authentication import SessionAuthentication
from rest_framework.exceptions import PermissionDenied
from rest_framework.generics import (
CreateAPIView,
ListAPIView,
Expand All @@ -28,6 +30,7 @@
IpRequestSerializer,
SubnetSerializer,
)
from .utils import validate_permission

IpAddress = swapper.load_model('openwisp_ipam', 'IpAddress')
Subnet = swapper.load_model('openwisp_ipam', 'Subnet')
Expand Down Expand Up @@ -145,6 +148,8 @@ class AvailableIpView(RetrieveAPIView):

def get(self, request, *args, **kwargs):
subnet = get_object_or_404(self.subnet_model, pk=self.kwargs['subnet_id'])
user = self.request.user
validate_permission(self, user, subnet.organization)
return Response(subnet.get_next_available_ip())


Expand All @@ -157,6 +162,8 @@ class IpAddressListCreateView(ListCreateAPIView):

def get_queryset(self):
subnet = get_object_or_404(self.subnet_model, pk=self.kwargs['subnet_id'])
user = self.request.user
validate_permission(self, user, subnet.organization)
return subnet.ipaddress_set.all().order_by('ip_address')


Expand All @@ -165,21 +172,34 @@ class SubnetListCreateView(ListCreateAPIView):
authentication_classes = (BearerAuthentication, SessionAuthentication)
permission_classes = (DjangoModelPermissions,)
pagination_class = ListViewPagination
queryset = Subnet.objects.all()

def get_queryset(self):
queryset = Subnet.objects.all()
user = self.request.user
if not user.is_superuser:
queryset = Subnet.objects.filter(organization__in=user.organizations_dict)
return queryset


class SubnetView(RetrieveUpdateDestroyAPIView):
serializer_class = SubnetSerializer
authentication_classes = (BearerAuthentication, SessionAuthentication)
permission_classes = (DjangoModelPermissions,)
permission_classes = (
IsOrganizationMember,
DjangoModelPermissions,
)
queryset = Subnet.objects.all()


class IpAddressView(RetrieveUpdateDestroyAPIView):
serializer_class = IpAddressSerializer
authentication_classes = (BearerAuthentication, SessionAuthentication)
permission_classes = (DjangoModelPermissions,)
permission_classes = (
IsOrganizationMember,
DjangoModelPermissions,
)
queryset = IpAddress.objects.all()
organization_field = 'subnet__organization'


class RequestIPView(CreateAPIView):
Expand All @@ -192,6 +212,8 @@ class RequestIPView(CreateAPIView):
def post(self, request, *args, **kwargs):
options = {'description': request.data.get('description')}
subnet = get_object_or_404(self.subnet_model, pk=kwargs['subnet_id'])
user = self.request.user
validate_permission(self, user, subnet.organization)
ip_address = subnet.request_ip(options)
if ip_address:
serializer = IpAddressSerializer(ip_address)
Expand All @@ -211,10 +233,11 @@ class ImportSubnetView(CreateAPIView):

def post(self, request, *args, **kwargs):
file = request.FILES['csvfile']
user = self.request.user
if not file.name.endswith(('.csv', '.xls', '.xlsx')):
return Response({'error': _('File type not supported.')}, status=400)
try:
self.subnet_model().import_csv(file)
self.subnet_model().import_csv(file, user)
except CsvImportException as e:
return Response({'error': _(str(e))}, status=400)
return Response({'detail': _('Data imported successfully.')})
Expand All @@ -228,6 +251,9 @@ class ExportSubnetView(CreateAPIView):
permission_classes = (DjangoModelPermissions,)

def post(self, request, *args, **kwargs):
subnet = get_object_or_404(self.subnet_model, pk=self.kwargs['subnet_id'])
user = self.request.user
validate_permission(self, user, subnet.organization)
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename="ip_address.csv"'
writer = csv.writer(response)
Expand All @@ -246,6 +272,8 @@ class SubnetHostsView(ListAPIView):
def get_queryset(self):
subnet = get_object_or_404(self.subnet_model, pk=self.kwargs['subnet_id'])
qs = HostsSet(subnet)
user = self.request.user
validate_permission(self, user, subnet.organization)
return qs


Expand Down
20 changes: 14 additions & 6 deletions openwisp_ipam/base/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from ipaddress import ip_address, ip_network

import xlrd
from django.core.exceptions import ValidationError
from django.core.exceptions import PermissionDenied, ValidationError
from django.db import models
from django.utils.translation import gettext_lazy as _
from openwisp_users.mixins import ShareableOrgMixin
Expand Down Expand Up @@ -136,11 +136,11 @@ def request_ip(self, options=None):
ip_address.save()
return ip_address

def _read_subnet_data(self, reader):
def _read_subnet_data(self, reader, *user):
subnet_model = load_model('openwisp_ipam', 'Subnet')
subnet_name = next(reader)[0].strip()
subnet_value = next(reader)[0].strip()
subnet_org = self._get_or_create_org(next(reader)[0].strip())
subnet_org = self._get_or_create_org(next(reader)[0].strip(), *user)
try:
subnet = subnet_model.objects.get(
subnet=subnet_value, organization=subnet_org
Expand Down Expand Up @@ -178,7 +178,7 @@ def _read_ipaddress_data(self, reader, subnet):
for ip in ipaddress_list:
ip.save()

def import_csv(self, file):
def import_csv(self, file, *user):
if file.name.endswith(('.xls', '.xlsx')):
book = xlrd.open_workbook(file_contents=file.read())
sheet = book.sheet_by_index(0)
Expand All @@ -188,11 +188,17 @@ def import_csv(self, file):
reader = iter(row)
else:
reader = csv.reader(StringIO(file.read().decode('utf-8')), delimiter=',')
subnet = self._read_subnet_data(reader)
subnet = self._read_subnet_data(reader, *user)
next(reader)
next(reader)
self._read_ipaddress_data(reader, subnet)

def validate_permission(self, org, user):
if not (user.is_superuser or user.is_manager(org)):
raise PermissionDenied(
"User does not have access to the specified organization"
)

def export_csv(self, subnet_id, writer):
ipaddress_model = load_model('openwisp_ipam', 'IpAddress')
subnet = load_model('openwisp_ipam', 'Subnet').objects.get(pk=subnet_id)
Expand All @@ -210,9 +216,11 @@ def export_csv(self, subnet_id, writer):
row.append(str(getattr(obj, field.name)))
writer.writerow(row)

def _get_or_create_org(self, org_name):
def _get_or_create_org(self, org_name, *user):
try:
instance = Organization.objects.get(name=org_name)
if user:
self.validate_permission(instance, *user)
except ValidationError as e:
raise CsvImportException(str(e))
except Organization.DoesNotExist:
Expand Down
Loading

0 comments on commit 8f36d31

Please sign in to comment.