diff --git a/modoboa/limits/forms.py b/modoboa/limits/forms.py index 2d78c75df..2958b201c 100644 --- a/modoboa/limits/forms.py +++ b/modoboa/limits/forms.py @@ -7,7 +7,7 @@ from django.utils.translation import ugettext as _ from modoboa.lib.permissions import get_object_owner -from .lib import BadLimitValue, UnsufficientResource +from .lib import allocate_resources_from_user from . import utils @@ -48,32 +48,6 @@ class ResourcePoolForm(forms.Form): self.fields[fieldname].initial = ( user.userobjectlimit_set.get(name=lname).max_value) - def allocate_from_user(self, limit, user): - """Allocate resource using an existing user. - - When a reseller creates a domain administrator, he generally - assigns him resource to create new objetcs. As a reseller may - also be limited, the resource he gives is taken from its own - pool. - """ - ol = user.userobjectlimit_set.get(name=limit.name) - fieldname = "{}_limit".format(limit.name) - newvalue = self.cleaned_data[fieldname] - if newvalue == -1 and ol.max_value != -1: - raise BadLimitValue( - _("You're not allowed to define unlimited values") - ) - - if limit.max_value > -1: - newvalue -= limit.max_value - if newvalue == 0: - return - remain = ol.max_value - ol.current_value - if newvalue > remain: - raise UnsufficientResource(ol) - ol.max_value -= newvalue - ol.save() - def save(self): owner = get_object_owner(self.account) for name, ltpl in utils.get_user_limit_templates(): @@ -82,9 +56,10 @@ class ResourcePoolForm(forms.Form): continue l = self.account.userobjectlimit_set.get(name=name) if not owner.is_superuser: - self.allocate_from_user(l, owner) + allocate_resources_from_user( + l, owner, self.cleaned_data[fieldname]) l.max_value = self.cleaned_data[fieldname] - l.save() + l.save(update_fields=["max_value"]) class DomainLimitsForm(forms.Form): diff --git a/modoboa/limits/lib.py b/modoboa/limits/lib.py index 6daaf3f90..35af3b584 100644 --- a/modoboa/limits/lib.py +++ b/modoboa/limits/lib.py @@ -31,3 +31,28 @@ class UnsufficientResource(ModoboaException): class BadLimitValue(ModoboaException): http_code = 400 + + +def allocate_resources_from_user(limit, user, value): + """Allocate resource using an existing user. + + When a reseller creates a domain administrator, he generally + assigns him resource to create new objetcs. As a reseller may + also be limited, the resource he gives is taken from its own + pool. + """ + ol = user.userobjectlimit_set.get(name=limit.name) + if value == -1 and ol.max_value != -1: + raise BadLimitValue( + _("You're not allowed to define unlimited values") + ) + + if limit.max_value > -1: + value -= limit.max_value + if value == 0: + return + remain = ol.max_value - ol.current_value + if value > remain: + raise UnsufficientResource(ol) + ol.max_value -= value + ol.save() diff --git a/modoboa/limits/serializers.py b/modoboa/limits/serializers.py new file mode 100644 index 000000000..c11205a92 --- /dev/null +++ b/modoboa/limits/serializers.py @@ -0,0 +1,33 @@ +"""Limits serializers.""" + +from rest_framework import serializers + +from . import lib + + +class ResourcesSerializer(serializers.Serializer): + """Resources serializer.""" + + domains = serializers.IntegerField(min_value=-1) + domain_aliases = serializers.IntegerField(min_value=-1) + mailboxes = serializers.IntegerField(min_value=-1) + mailbox_aliases = serializers.IntegerField(min_value=-1) + domain_admins = serializers.IntegerField(min_value=-1) + quota = serializers.IntegerField(min_value=0) + + def to_representation(self, instance): + """Return limits.""" + return dict( + (limit.name, limit.max_value) + for limit in instance.userobjectlimit_set.all()) + + def update(self, instance, validated_data): + """Update limits.""" + user = self.context["request"].user + for name, max_value in validated_data.items(): + limit = instance.userobjectlimit_set.get(name=name) + if not user.is_superuser: + lib.allocate_resources_from_user(limit, user, max_value) + limit.max_value = max_value + limit.save(update_fields=["max_value"]) + return instance diff --git a/modoboa/limits/tests/test_api.py b/modoboa/limits/tests/test_api.py index ec2fb6d2c..5917c777f 100644 --- a/modoboa/limits/tests/test_api.py +++ b/modoboa/limits/tests/test_api.py @@ -1,6 +1,8 @@ # coding: utf-8 """Test cases for the limits extension.""" +from testfixtures import compare + from django.core.urlresolvers import reverse from rest_framework.authtoken.models import Token @@ -9,6 +11,7 @@ from modoboa.admin.factories import populate_database from modoboa.admin.models import Domain from modoboa.core import factories as core_factories from modoboa.core.models import User +from modoboa.lib import permissions from modoboa.lib import tests as lib_tests from .. import utils @@ -253,3 +256,88 @@ class APIDomainLimitsTestCase(lib_tests.ModoAPITestCase): } response = self.client.post(url, data, format="json") self.assertEqual(response.status_code, 400) + + +class ResourcesAPITestCase(lib_tests.ModoAPITestCase): + """Check resources API.""" + + @classmethod + def setUpTestData(cls): + """Create test data.""" + super(ResourcesAPITestCase, cls).setUpTestData() + for name, tpl in utils.get_user_limit_templates(): + cls.localconfig.parameters.set_value( + "deflt_user_{0}_limit".format(name), 2) + cls.localconfig.save() + populate_database() + cls.user = User.objects.get(username="admin@test.com") + cls.da_token = Token.objects.create(user=cls.user) + cls.reseller = core_factories.UserFactory( + username="reseller", groups=("Resellers", ), + ) + cls.r_token = Token.objects.create(user=cls.reseller) + + def test_get_admin_resources(self): + """Retrieve admin resources.""" + url = reverse("api:resources-detail", args=[self.user.pk]) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + expected = { + "quota": 2, + "mailboxes": 2, + "domain_admins": 2, + "domain_aliases": 2, + "domains": 2, + "mailbox_aliases": 2 + } + compare(expected, response.data) + + # As reseller => fails + self.client.credentials( + HTTP_AUTHORIZATION="Token {}".format(self.r_token.key)) + response = self.client.get(url) + self.assertEqual(response.status_code, 404) + + # As domain admin => fails + self.client.credentials( + HTTP_AUTHORIZATION="Token {}".format(self.da_token.key)) + response = self.client.get(url) + self.assertEqual(response.status_code, 404) + + def test_update_resources(self): + """Update resources.""" + url = reverse("api:resources-detail", args=[self.reseller.pk]) + response = self.client.get(url) + resources = response.data + resources.update({"domains": 1000, "mailboxes": 1000}) + response = self.client.put(url, resources) + self.assertEqual(response.status_code, 200) + self.assertEqual( + self.reseller.userobjectlimit_set.get(name="domains").max_value, + 1000) + + # As domain admin => fails + self.client.credentials( + HTTP_AUTHORIZATION="Token {}".format(self.da_token.key)) + resources.update({"domains": 2, "mailboxes": 2}) + url = reverse("api:resources-detail", args=[self.user.pk]) + response = self.client.put(url, resources) + self.assertEqual(response.status_code, 404) + + # As reseller => ok + permissions.grant_access_to_object(self.reseller, self.user, True) + self.client.credentials( + HTTP_AUTHORIZATION="Token {}".format(self.r_token.key)) + resources.update({"domains": 500, "mailboxes": 500}) + url = reverse("api:resources-detail", args=[self.user.pk]) + response = self.client.put(url, resources) + self.assertEqual(response.status_code, 200) + self.assertEqual( + self.user.userobjectlimit_set.get(name="domains").max_value, + 500) + self.assertEqual( + self.reseller.userobjectlimit_set.get(name="domains").max_value, + 502) + resources.update({"domains": 1003}) + response = self.client.put(url, resources) + self.assertEqual(response.status_code, 424) diff --git a/modoboa/limits/urls_api.py b/modoboa/limits/urls_api.py new file mode 100644 index 000000000..de97d5718 --- /dev/null +++ b/modoboa/limits/urls_api.py @@ -0,0 +1,12 @@ +"""Limits API urls.""" + +from rest_framework import routers + +from . import viewsets + + +router = routers.SimpleRouter() +router.register( + r"resources", viewsets.ResourcesViewSet, base_name="resources") + +urlpatterns = router.urls diff --git a/modoboa/limits/viewsets.py b/modoboa/limits/viewsets.py new file mode 100644 index 000000000..1b7fff120 --- /dev/null +++ b/modoboa/limits/viewsets.py @@ -0,0 +1,31 @@ +"""Limits API.""" + +from django.contrib.contenttypes.models import ContentType + +from rest_framework import mixins, viewsets +from rest_framework.permissions import IsAuthenticated, DjangoModelPermissions + +from modoboa.core import models as core_models + +from . import serializers + + +class ResourcesViewSet( + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + viewsets.GenericViewSet): + """Resources viewset.""" + + permission_classes = [IsAuthenticated, DjangoModelPermissions] + serializer_class = serializers.ResourcesSerializer + + def get_queryset(self): + """Filter queryset based on current user.""" + user = self.request.user + ids = list( + user.objectaccess_set.filter( + content_type=ContentType.objects.get_for_model(user)) + .values_list("object_id", flat=True)) + if not user.is_superuser and user.pk in ids: + ids.remove(user.pk) + return core_models.User.objects.filter(pk__in=ids) diff --git a/modoboa/urls_api.py b/modoboa/urls_api.py index 8261317b3..64d6aaa2f 100644 --- a/modoboa/urls_api.py +++ b/modoboa/urls_api.py @@ -6,6 +6,7 @@ from modoboa.core.extensions import exts_pool urlpatterns = [ - url("", include("modoboa.admin.urls_api")) + url("", include("modoboa.admin.urls_api")), + url("", include("modoboa.limits.urls_api")), ] urlpatterns += exts_pool.get_urls(category="api")