[RFC 2/2] REST: Add Projects to the REST API

Finucane, Stephen stephen.finucane at intel.com
Fri Mar 25 02:47:47 AEDT 2016


On 16 Mar 11:13, Andy Doan wrote:
> This exports projects via the REST API.
> 
> Security Constraints:
>  * Anyone (logged in or not) can read all projects.
>  * Only admins can create/delete projects.
>  * Project maintainers are allowed to update (ie "patch" project
>    attributes)

Assume you want this reviewed, so comments below. Generally like the
idea though _if_ we can approach it bit by bit :)

Stephen

> Signed-off-by: Andy Doan <andy.doan at linaro.org>
> Inspired-by: Damien Lespiau <damien.lespiau at intel.com>
> ---
>  patchwork/tests/test_rest_api.py | 126 +++++++++++++++++++++++++++++++++++++++
>  patchwork/views/rest_api.py      |  32 +++++++++-
>  2 files changed, 157 insertions(+), 1 deletion(-)
>  create mode 100644 patchwork/tests/test_rest_api.py
> 
> diff --git a/patchwork/tests/test_rest_api.py b/patchwork/tests/test_rest_api.py
> new file mode 100644
> index 0000000..64c7615
> --- /dev/null
> +++ b/patchwork/tests/test_rest_api.py
> @@ -0,0 +1,126 @@
> +# Patchwork - automated patch tracking system
> +# Copyright (C) 2008 Jeremy Kerr <jk at ozlabs.org>

nit: Is this needed?

> +# Copyright (C) 2016 Linaro Corporation
> +#
> +# This file is part of the Patchwork package.
> +#
> +# Patchwork is free software; you can redistribute it and/or modify
> +# it under the terms of the GNU General Public License as published by
> +# the Free Software Foundation; either version 2 of the License, or
> +# (at your option) any later version.
> +#
> +# Patchwork is distributed in the hope that it will be useful,
> +# but WITHOUT ANY WARRANTY; without even the implied warranty of
> +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
> +# GNU General Public License for more details.
> +#
> +# You should have received a copy of the GNU General Public License
> +# along with Patchwork; if not, write to the Free Software
> +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
> +
> +import unittest
> +
> +from django.conf import settings
> +from django.test import TestCase
> +
> +from rest_framework.test import APIClient
> +
> +from patchwork.models import Project
> +from patchwork.tests.utils import defaults, create_maintainer, create_user
> +
> +
> + at unittest.skipUnless(settings.ENABLE_REST_API, 'requires ENABLE_REST_API')
> +class TestProjectAPI(TestCase):
> +    fixtures = ['default_states']
> +
> +    def test_list_simple(self):
> +        """Validate we can list the default test project."""
> +        defaults.project.save()
> +        client = APIClient()
> +        resp = client.get('/api/1.0/projects/')
> +        self.assertEqual(200, resp.status_code)
> +        self.assertEqual(1, len(resp.data))
> +        proj = resp.data[0]
> +        self.assertEqual(defaults.project.linkname, proj['linkname'])
> +        self.assertEqual(defaults.project.name, proj['name'])
> +        self.assertEqual(defaults.project.listid, proj['listid'])
> +
> +    def test_get(self):
> +        """Validate we can get a specific project."""
> +        defaults.project.save()
> +        client = APIClient()
> +        resp = client.get('/api/1.0/projects/1/')
> +        self.assertEqual(200, resp.status_code)
> +        self.assertEqual(defaults.project.name, resp.data['name'])
> +
> +    def test_anonymous_writes(self):
> +        """Ensure anonymous "write" operations are rejected."""
> +        defaults.project.save()
> +        client = APIClient()
> +        # create
> +        resp = client.post(
> +            '/api/1.0/projects/',
> +            {'linkname': 'l', 'name': 'n', 'listid': 'l', 'listemail': 'e'})
> +        self.assertEqual(403, resp.status_code)
> +        # update
> +        resp = client.patch('/api/1.0/projects/1/', {'linkname': 'foo'})
> +        self.assertEqual(403, resp.status_code)
> +        # delete
> +        resp = client.delete('/api/1.0/projects/1/')
> +        self.assertEqual(403, resp.status_code)
> +
> +    def test_create(self):
> +        """Ensure creations can be performed admins."""
> +        defaults.project.save()
> +        client = APIClient()
> +
> +        user = create_maintainer(defaults.project)
> +        client.force_authenticate(user=user)
> +        resp = client.post(
> +            '/api/1.0/projects/',
> +            {'linkname': 'l', 'name': 'n', 'listid': 'l', 'listemail': 'e'})
> +        self.assertEqual(403, resp.status_code)
> +
> +        user.is_superuser = True
> +        user.save()
> +        client.force_authenticate(user=user)
> +        resp = client.post(
> +            '/api/1.0/projects/',
> +            {'linkname': 'l', 'name': 'n', 'listid': 'l', 'listemail': 'e'})
> +        self.assertEqual(201, resp.status_code)
> +        self.assertEqual(2, Project.objects.all().count())
> +
> +    def test_update(self):
> +        """Ensure updates can be performed maintainers."""
> +        defaults.project.save()
> +        client = APIClient()
> +
> +        # A maintainer can update
> +        user = create_maintainer(defaults.project)
> +        client.force_authenticate(user=user)
> +        resp = client.patch('/api/1.0/projects/1/', {'linkname': 'TEST'})
> +        self.assertEqual(200, resp.status_code)
> +
> +        # A normal user can't
> +        user = create_user()
> +        client.force_authenticate(user=user)
> +        resp = client.patch('/api/1.0/projects/1/', {'linkname': 'TEST'})
> +        self.assertEqual(403, resp.status_code)
> +
> +    def test_delete(self):
> +        """Ensure deletions can be performed admins."""
> +        defaults.project.save()
> +        client = APIClient()
> +
> +        # A maintainer can't remove a project
> +        user = create_maintainer(defaults.project)
> +        client.force_authenticate(user=user)
> +        resp = client.delete('/api/1.0/projects/1/')
> +        self.assertEqual(403, resp.status_code)
> +
> +        # An admin can
> +        user.is_superuser = True
> +        user.save()
> +        client.force_authenticate(user=user)
> +        resp = client.delete('/api/1.0/projects/1/')
> +        self.assertEqual(204, resp.status_code)

So I know DRF provides its own 'APITestCase' class. Would it beneficial
to use this?

* http://www.django-rest-framework.org/api-guide/testing/#test-cases
* http://stackoverflow.com/q/28221754/613428

> diff --git a/patchwork/views/rest_api.py b/patchwork/views/rest_api.py
> index a39bcf4..20d4523 100644
> --- a/patchwork/views/rest_api.py
> +++ b/patchwork/views/rest_api.py
> @@ -18,6 +18,36 @@
>  # along with Patchwork; if not, write to the Free Software
>  # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
>  
> -from rest_framework import routers
> +from patchwork.models import Project
> +
> +from rest_framework import permissions, routers, serializers, viewsets
> +
> +
> +class MaintainerPermission(permissions.BasePermission):
> +    def has_permission(self, request, view):
> +        if request.method in ('POST', 'DELETE'):
> +            # only administrators can create and delete entities
> +            user = request.user
> +            return user.is_authenticated() and user.is_superuser
> +        return super(MaintainerPermission, self).has_permission(request, view)
> +
> +    def has_object_permission(self, request, view, obj):
> +        # read only for everyone
> +        if request.method in permissions.SAFE_METHODS:
> +            return True
> +        return obj.is_editable(request.user)
> +
> +
> +class ProjectSerializer(serializers.HyperlinkedModelSerializer):
> +    class Meta:
> +        model = Project

Should we serialize 'tags' also (read only)?

> +
> +
> +class ProjectViewSet(viewsets.ModelViewSet):
> +    permission_classes = (MaintainerPermission, )
> +    queryset = Project.objects.all()
> +    serializer_class = ProjectSerializer
> +
>  
>  router = routers.DefaultRouter()
> +router.register('projects', ProjectViewSet)
> -- 
> 2.7.0
> 
> _______________________________________________
> Patchwork mailing list
> Patchwork at lists.ozlabs.org
> https://lists.ozlabs.org/listinfo/patchwork


More information about the Patchwork mailing list