diff --git a/InvenTree/InvenTree/api_tester.py b/InvenTree/InvenTree/api_tester.py new file mode 100644 index 0000000000..2e69e40969 --- /dev/null +++ b/InvenTree/InvenTree/api_tester.py @@ -0,0 +1,94 @@ +""" +Helper functions for performing API unit tests +""" + +from django.contrib.auth import get_user_model +from django.contrib.auth.models import Group +from rest_framework.test import APITestCase + + +class InvenTreeAPITestCase(APITestCase): + """ + Base class for running InvenTree API tests + """ + + # User information + username = 'testuser' + password = 'mypassword' + email = 'test@testing.com' + + superuser = False + auto_login = True + + # Set list of roles automatically associated with the user + roles = [] + + def setUp(self): + + super().setUp() + + # Create a user to log in with + self.user = get_user_model().objects.create_user( + username=self.username, + password=self.password, + email=self.email + ) + + # Create a group for the user + self.group = Group.objects.create(name='my_test_group') + self.user.groups.add(self.group) + + if self.superuser: + self.user.is_superuser = True + self.user.save() + + for role in self.roles: + self.assignRole(role) + + if self.auto_login: + self.client.login(username=self.username, password=self.password) + + def assignRole(self, role): + """ + Set the user roles for the registered user + """ + + # role is of the format 'rule.permission' e.g. 'part.add' + + rule, perm = role.split('.') + + for ruleset in self.group.rule_sets.all(): + + if ruleset.name == rule: + + if perm == 'view': + ruleset.can_view = True + elif perm == 'change': + ruleset.can_change = True + elif perm == 'delete': + ruleset.can_delete = True + elif perm == 'add': + ruleset.can_add = True + + ruleset.save() + break + + def get(self, url, data={}, code=200): + """ + Issue a GET request + """ + + response = self.client.get(url, data, format='json') + + self.assertEqual(response.status_code, code) + + return response + + def post(self, url, data): + """ + Issue a POST request + """ + + response = self.client.post(url, data=data, format='json') + + return response diff --git a/InvenTree/InvenTree/permissions.py b/InvenTree/InvenTree/permissions.py new file mode 100644 index 0000000000..836943b7a8 --- /dev/null +++ b/InvenTree/InvenTree/permissions.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from rest_framework import permissions + +import users.models + + +class RolePermission(permissions.BasePermission): + """ + Role mixin for API endpoints, allowing us to specify the user "role" + which is required for certain operations. + + Each endpoint can have one or more of the following actions: + - GET + - POST + - PUT + - PATCH + - DELETE + + Specify the required "role" using the role_required attribute. + + e.g. + + role_required = "part" + + The RoleMixin class will then determine if the user has the required permission + to perform the specified action. + + For example, a DELETE action will be rejected unless the user has the "part.remove" permission + + """ + + def has_permission(self, request, view): + """ + Determine if the current user has the specified permissions + """ + + # First, check that the user is authenticated! + auth = permissions.IsAuthenticated() + + if not auth.has_permission(request, view): + return False + + user = request.user + + # Superuser can do it all + if user.is_superuser: + return True + + # Map the request method to a permission type + rolemap = { + 'GET': 'view', + 'OPTIONS': 'view', + 'POST': 'add', + 'PUT': 'change', + 'PATCH': 'change', + 'DELETE': 'delete', + } + + permission = rolemap[request.method] + + try: + # Extract the model name associated with this request + model = view.serializer_class.Meta.model + + # And the specific database table + table = model._meta.db_table + except AttributeError: + # We will assume that if the serializer class does *not* have a Meta, + # then we don't need a permission + return True + + result = users.models.RuleSet.check_table_permission(user, table, permission) + + return result diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 70760624c6..5dbfb845bc 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -278,6 +278,7 @@ REST_FRAMEWORK = { 'DEFAULT_PERMISSION_CLASSES': ( 'rest_framework.permissions.IsAuthenticated', 'rest_framework.permissions.DjangoModelPermissions', + 'InvenTree.permissions.RolePermission', ), 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema' } diff --git a/InvenTree/InvenTree/test_api.py b/InvenTree/InvenTree/test_api.py index f44542656f..52765db2a7 100644 --- a/InvenTree/InvenTree/test_api.py +++ b/InvenTree/InvenTree/test_api.py @@ -1,15 +1,17 @@ """ Low level tests for the InvenTree API """ -from rest_framework.test import APITestCase from rest_framework import status from django.urls import reverse -from django.contrib.auth import get_user_model + +from InvenTree.api_tester import InvenTreeAPITestCase + +from users.models import RuleSet from base64 import b64encode -class APITests(APITestCase): +class APITests(InvenTreeAPITestCase): """ Tests for the InvenTree API """ fixtures = [ @@ -19,15 +21,13 @@ class APITests(APITestCase): 'category', ] - username = 'test_user' - password = 'test_pass' - token = None + auto_login = False + def setUp(self): - # Create a user (but do not log in!) - get_user_model().objects.create_user(self.username, 'user@email.com', self.password) + super().setUp() def basicAuth(self): # Use basic authentication @@ -78,3 +78,82 @@ class APITests(APITestCase): self.assertIn('instance', data) self.assertEquals('InvenTree', data['server']) + + def test_role_view(self): + """ + Test that we can access the 'roles' view for the logged in user. + + Also tests that it is *not* accessible if the client is not logged in. + """ + + url = reverse('api-user-roles') + + response = self.client.get(url, format='json') + + # Not logged in, so cannot access user role data + self.assertTrue(response.status_code in [401, 403]) + + # Now log in! + self.basicAuth() + + response = self.get(url) + + data = response.data + + self.assertIn('user', data) + self.assertIn('username', data) + self.assertIn('is_staff', data) + self.assertIn('is_superuser', data) + self.assertIn('roles', data) + + roles = data['roles'] + + role_names = roles.keys() + + # By default, 'view' permissions are provided + for rule in RuleSet.RULESET_NAMES: + self.assertIn(rule, role_names) + + self.assertIn('view', roles[rule]) + + self.assertNotIn('add', roles[rule]) + self.assertNotIn('change', roles[rule]) + self.assertNotIn('delete', roles[rule]) + + def test_with_superuser(self): + """ + Superuser should have *all* roles assigned + """ + + self.user.is_superuser = True + self.user.save() + + self.basicAuth() + + response = self.get(reverse('api-user-roles')) + + roles = response.data['roles'] + + for rule in RuleSet.RULESET_NAMES: + self.assertIn(rule, roles.keys()) + + for perm in ['view', 'add', 'change', 'delete']: + self.assertIn(perm, roles[rule]) + + def test_with_roles(self): + """ + Assign some roles to the user + """ + + self.basicAuth() + response = self.get(reverse('api-user-roles')) + + self.assignRole('part.delete') + self.assignRole('build.change') + response = self.get(reverse('api-user-roles')) + + roles = response.data['roles'] + + # New role permissions should have been added now + self.assertIn('delete', roles['part']) + self.assertIn('change', roles['build']) diff --git a/InvenTree/build/test_api.py b/InvenTree/build/test_api.py index bcfd600e9e..02bcde6bb4 100644 --- a/InvenTree/build/test_api.py +++ b/InvenTree/build/test_api.py @@ -3,19 +3,16 @@ from __future__ import unicode_literals from datetime import datetime, timedelta -from rest_framework.test import APITestCase - from django.urls import reverse -from django.contrib.auth import get_user_model -from django.contrib.auth.models import Group from part.models import Part from build.models import Build from InvenTree.status_codes import BuildStatus +from InvenTree.api_tester import InvenTreeAPITestCase -class BuildAPITest(APITestCase): +class BuildAPITest(InvenTreeAPITestCase): """ Series of tests for the Build DRF API """ @@ -27,33 +24,16 @@ class BuildAPITest(APITestCase): 'bom', 'build', ] + + # Required roles to access Build API endpoints + roles = [ + 'build.change', + 'build.add' + ] def setUp(self): - # Create a user for auth - user = get_user_model() - - self.user = user.objects.create_user( - username='testuser', - email='test@testing.com', - password='password' - ) - # Put the user into a group with the correct permissions - group = Group.objects.create(name='mygroup') - self.user.groups.add(group) - - # Give the group *all* the permissions! - for rule in group.rule_sets.all(): - rule.can_view = True - rule.can_change = True - rule.can_add = True - rule.can_delete = True - - rule.save() - - group.save() - - self.client.login(username='testuser', password='password') + super().setUp() class BuildListTest(BuildAPITest): @@ -63,34 +43,26 @@ class BuildListTest(BuildAPITest): url = reverse('api-build-list') - def get(self, status_code=200, data={}): - - response = self.client.get(self.url, data, format='json') - - self.assertEqual(response.status_code, status_code) - - return response.data - def test_get_all_builds(self): """ Retrieve *all* builds via the API """ - builds = self.get() + builds = self.get(self.url) - self.assertEqual(len(builds), 5) + self.assertEqual(len(builds.data), 5) - builds = self.get(data={'active': True}) - self.assertEqual(len(builds), 1) + builds = self.get(self.url, data={'active': True}) + self.assertEqual(len(builds.data), 1) - builds = self.get(data={'status': BuildStatus.COMPLETE}) - self.assertEqual(len(builds), 4) + builds = self.get(self.url, data={'status': BuildStatus.COMPLETE}) + self.assertEqual(len(builds.data), 4) - builds = self.get(data={'overdue': False}) - self.assertEqual(len(builds), 5) + builds = self.get(self.url, data={'overdue': False}) + self.assertEqual(len(builds.data), 5) - builds = self.get(data={'overdue': True}) - self.assertEqual(len(builds), 0) + builds = self.get(self.url, data={'overdue': True}) + self.assertEqual(len(builds.data), 0) def test_overdue(self): """ @@ -109,7 +81,9 @@ class BuildListTest(BuildAPITest): target_date=in_the_past ) - builds = self.get(data={'overdue': True}) + response = self.get(self.url, data={'overdue': True}) + + builds = response.data self.assertEqual(len(builds), 1) @@ -152,11 +126,15 @@ class BuildListTest(BuildAPITest): Build.objects.rebuild() # Search by parent - builds = self.get(data={'parent': parent.pk}) + response = self.get(self.url, data={'parent': parent.pk}) + + builds = response.data self.assertEqual(len(builds), 5) # Search by ancestor - builds = self.get(data={'ancestor': parent.pk}) + response = self.get(self.url, data={'ancestor': parent.pk}) + + builds = response.data self.assertEqual(len(builds), 20) diff --git a/InvenTree/company/test_api.py b/InvenTree/company/test_api.py index f466a4a223..219a8ac019 100644 --- a/InvenTree/company/test_api.py +++ b/InvenTree/company/test_api.py @@ -1,32 +1,24 @@ -from rest_framework.test import APITestCase from rest_framework import status from django.urls import reverse -from django.contrib.auth import get_user_model -from InvenTree.helpers import addUserPermissions +from InvenTree.api_tester import InvenTreeAPITestCase from .models import Company -class CompanyTest(APITestCase): +class CompanyTest(InvenTreeAPITestCase): """ Series of tests for the Company DRF API """ - def setUp(self): - # Create a user for auth - user = get_user_model() - self.user = user.objects.create_user('testuser', 'test@testing.com', 'password') - - perms = [ - 'view_company', - 'change_company', - 'add_company', - ] + roles = [ + 'purchase_order.add', + 'purchase_order.change', + ] - addUserPermissions(self.user, perms) - - self.client.login(username='testuser', password='password') + def setUp(self): + + super().setUp() Company.objects.create(name='ACME', description='Supplier', is_customer=False, is_supplier=True) Company.objects.create(name='Drippy Cup Co.', description='Customer', is_customer=True, is_supplier=False) @@ -36,24 +28,24 @@ class CompanyTest(APITestCase): url = reverse('api-company-list') # There should be two companies - response = self.client.get(url, format='json') + response = self.get(url) self.assertEqual(len(response.data), 3) data = {'is_customer': True} # There should only be one customer - response = self.client.get(url, data, format='json') + response = self.get(url, data) self.assertEqual(len(response.data), 1) data = {'is_supplier': True} # There should be two suppliers - response = self.client.get(url, data, format='json') + response = self.get(url, data) self.assertEqual(len(response.data), 2) def test_company_detail(self): url = reverse('api-company-detail', kwargs={'pk': 1}) - response = self.client.get(url, format='json') + response = self.get(url) self.assertEqual(response.data['name'], 'ACME') @@ -68,5 +60,5 @@ class CompanyTest(APITestCase): # Test search functionality in company list url = reverse('api-company-list') data = {'search': 'cup'} - response = self.client.get(url, data, format='json') + response = self.get(url, data) self.assertEqual(len(response.data), 2) diff --git a/InvenTree/label/test_api.py b/InvenTree/label/test_api.py index 1fc0a3e0da..92e7733891 100644 --- a/InvenTree/label/test_api.py +++ b/InvenTree/label/test_api.py @@ -3,13 +3,12 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from rest_framework.test import APITestCase - from django.urls import reverse -from django.contrib.auth import get_user_model + +from InvenTree.api_tester import InvenTreeAPITestCase -class TestReportTests(APITestCase): +class TestReportTests(InvenTreeAPITestCase): """ Tests for the StockItem TestReport templates """ @@ -21,17 +20,16 @@ class TestReportTests(APITestCase): 'stock', ] + roles = [ + 'stock.view', + 'stock_location.view', + ] + list_url = reverse('api-stockitem-testreport-list') def setUp(self): - user = get_user_model() - self.user = user.objects.create_user('testuser', 'test@testing.com', 'password') - - self.user.is_staff = True - self.user.save() - - self.client.login(username='testuser', password='password') + super().setUp() def do_list(self, filters={}): diff --git a/InvenTree/order/test_api.py b/InvenTree/order/test_api.py index 58599f1eb3..cb92b8b384 100644 --- a/InvenTree/order/test_api.py +++ b/InvenTree/order/test_api.py @@ -4,16 +4,16 @@ Tests for the Order API from datetime import datetime, timedelta -from rest_framework.test import APITestCase from rest_framework import status from django.urls import reverse -from django.contrib.auth import get_user_model + +from InvenTree.api_tester import InvenTreeAPITestCase from .models import PurchaseOrder, SalesOrder -class OrderTest(APITestCase): +class OrderTest(InvenTreeAPITestCase): fixtures = [ 'category', @@ -26,25 +26,20 @@ class OrderTest(APITestCase): 'sales_order', ] + roles = [ + 'purchase_order.change', + 'sales_order.change', + ] + def setUp(self): - - # Create a user for auth - get_user_model().objects.create_user('testuser', 'test@testing.com', 'password') - self.client.login(username='testuser', password='password') - - def doGet(self, url, data={}): - - return self.client.get(url, data=data, format='json') - - def doPost(self, url, data={}): - return self.client.post(url, data=data, format='json') + super().setUp() def filter(self, filters, count): """ Test API filters """ - response = self.doGet( + response = self.get( self.LIST_URL, filters ) @@ -98,7 +93,7 @@ class PurchaseOrderTest(OrderTest): url = '/api/order/po/1/' - response = self.doGet(url) + response = self.get(url) self.assertEqual(response.status_code, 200) @@ -111,7 +106,7 @@ class PurchaseOrderTest(OrderTest): url = reverse('api-po-attachment-list') - response = self.doGet(url) + response = self.get(url) self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -161,7 +156,7 @@ class SalesOrderTest(OrderTest): url = '/api/order/so/1/' - response = self.doGet(url) + response = self.get(url) self.assertEqual(response.status_code, 200) @@ -173,6 +168,6 @@ class SalesOrderTest(OrderTest): url = reverse('api-so-attachment-list') - response = self.doGet(url) + response = self.get(url) self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 88d691797e..9a0e3a2a68 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -12,12 +12,12 @@ from django.db.models import Q, F, Count, Prefetch, Sum from rest_framework import status from rest_framework.response import Response from rest_framework import filters, serializers -from rest_framework import generics, permissions +from rest_framework import generics from django.conf.urls import url, include from django.urls import reverse -from .models import Part, PartCategory, BomItem, PartStar +from .models import Part, PartCategory, BomItem from .models import PartParameter, PartParameterTemplate from .models import PartAttachment, PartTestTemplate from .models import PartSellPriceBreak @@ -30,6 +30,7 @@ from . import serializers as part_serializers from InvenTree.views import TreeSerializer from InvenTree.helpers import str2bool, isNull from InvenTree.api import AttachmentMixin + from InvenTree.status_codes import BuildStatus @@ -37,6 +38,8 @@ class PartCategoryTree(TreeSerializer): title = "Parts" model = PartCategory + + queryset = PartCategory.objects.all() @property def root_url(self): @@ -45,10 +48,6 @@ class PartCategoryTree(TreeSerializer): def get_items(self): return PartCategory.objects.all().prefetch_related('parts', 'children') - permission_classes = [ - permissions.IsAuthenticated, - ] - class CategoryList(generics.ListCreateAPIView): """ API endpoint for accessing a list of PartCategory objects. @@ -689,55 +688,6 @@ class PartList(generics.ListCreateAPIView): ] -class PartStarDetail(generics.RetrieveDestroyAPIView): - """ API endpoint for viewing or removing a PartStar object """ - - queryset = PartStar.objects.all() - serializer_class = part_serializers.PartStarSerializer - - -class PartStarList(generics.ListCreateAPIView): - """ API endpoint for accessing a list of PartStar objects. - - - GET: Return list of PartStar objects - - POST: Create a new PartStar object - """ - - queryset = PartStar.objects.all() - serializer_class = part_serializers.PartStarSerializer - - def create(self, request, *args, **kwargs): - - # Override the user field (with the logged-in user) - data = request.data.copy() - data['user'] = str(request.user.id) - - serializer = self.get_serializer(data=data) - - serializer.is_valid(raise_exception=True) - self.perform_create(serializer) - headers = self.get_success_headers(serializer.data) - return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) - - permission_classes = [ - permissions.IsAuthenticated, - ] - - filter_backends = [ - DjangoFilterBackend, - filters.SearchFilter - ] - - filter_fields = [ - 'part', - 'user', - ] - - search_fields = [ - 'partname' - ] - - class PartParameterTemplateList(generics.ListCreateAPIView): """ API endpoint for accessing a list of PartParameterTemplate objects. @@ -975,12 +925,6 @@ part_api_urls = [ url(r'^attachment/', include([ url(r'^$', PartAttachmentList.as_view(), name='api-part-attachment-list'), ])), - - # Base URL for PartStar API endpoints - url(r'^star/', include([ - url(r'^(?P\d+)/?', PartStarDetail.as_view(), name='api-part-star-detail'), - url(r'^$', PartStarList.as_view(), name='api-part-star-list'), - ])), # Base URL for part sale pricing url(r'^sale-price/', include([ diff --git a/InvenTree/part/templates/part/bom.html b/InvenTree/part/templates/part/bom.html index 54f069257b..f7b366bccb 100644 --- a/InvenTree/part/templates/part/bom.html +++ b/InvenTree/part/templates/part/bom.html @@ -111,34 +111,25 @@ { accept: function() { - // Delete each row one at a time! - function deleteRow(idx) { + // Keep track of each DELETE request + var requests = []; - if (idx >= rows.length) { - // All selected rows deleted - reload the table - $("#bom-table").bootstrapTable('refresh'); - } + rows.forEach(function(row) { + requests.push( + inventreeDelete( + `/api/bom/${row.pk}/`, + ) + ); + }); - var row = rows[idx]; - - var url = `/api/bom/${row.pk}/`; - - inventreeDelete( - url, - { - complete: function(xhr, status) { - deleteRow(idx + 1); - } - } - ) - } - - // Start the deletion! - deleteRow(0); + // Wait for *all* the requests to complete + $.when.apply($, requests).then(function() { + $('#bom-table').bootstrapTable('refresh'); + }); } } ); - }); + }); $('#bom-upload').click(function() { location.href = "{% url 'upload-bom' part.id %}"; diff --git a/InvenTree/part/templates/part/category.html b/InvenTree/part/templates/part/category.html index f71d8c7578..0d1d11e7fd 100644 --- a/InvenTree/part/templates/part/category.html +++ b/InvenTree/part/templates/part/category.html @@ -138,13 +138,14 @@

{% block heading %} - + {% trans "Part Categories" %} {% endblock %}

{% block details %} - + +
{% endblock %}
diff --git a/InvenTree/part/test_api.py b/InvenTree/part/test_api.py index e31512e1d8..d917b6ebb2 100644 --- a/InvenTree/part/test_api.py +++ b/InvenTree/part/test_api.py @@ -1,18 +1,16 @@ -from rest_framework.test import APITestCase from rest_framework import status from django.urls import reverse -from django.contrib.auth import get_user_model -from django.contrib.auth.models import Group from part.models import Part from stock.models import StockItem from company.models import Company +from InvenTree.api_tester import InvenTreeAPITestCase from InvenTree.status_codes import StockStatus -class PartAPITest(APITestCase): +class PartAPITest(InvenTreeAPITestCase): """ Series of tests for the Part DRF API - Tests for Part API @@ -27,32 +25,16 @@ class PartAPITest(APITestCase): 'test_templates', ] + roles = [ + 'part.change', + 'part.add', + 'part.delete', + 'part_category.change', + 'part_category.add', + ] + def setUp(self): - # Create a user for auth - user = get_user_model() - - self.user = user.objects.create_user( - username='testuser', - email='test@testing.com', - password='password' - ) - - # Put the user into a group with the correct permissions - group = Group.objects.create(name='mygroup') - self.user.groups.add(group) - - # Give the group *all* the permissions! - for rule in group.rule_sets.all(): - rule.can_view = True - rule.can_change = True - rule.can_add = True - rule.can_delete = True - - rule.save() - - group.save() - - self.client.login(username='testuser', password='password') + super().setUp() def test_get_categories(self): """ Test that we can retrieve list of part categories """ @@ -254,7 +236,7 @@ class PartAPITest(APITestCase): self.assertEqual(response.status_code, status.HTTP_200_OK) -class PartAPIAggregationTest(APITestCase): +class PartAPIAggregationTest(InvenTreeAPITestCase): """ Tests to ensure that the various aggregation annotations are working correctly... """ @@ -268,13 +250,14 @@ class PartAPIAggregationTest(APITestCase): 'test_templates', ] - def setUp(self): - # Create a user for auth - user = get_user_model() - - user.objects.create_user('testuser', 'test@testing.com', 'password') + roles = [ + 'part.view', + 'part.change', + ] - self.client.login(username='testuser', password='password') + def setUp(self): + + super().setUp() # Add a new part self.part = Part.objects.create( diff --git a/InvenTree/stock/templates/stock/location.html b/InvenTree/stock/templates/stock/location.html index 817b096f74..5c2b4e04db 100644 --- a/InvenTree/stock/templates/stock/location.html +++ b/InvenTree/stock/templates/stock/location.html @@ -48,15 +48,14 @@
  • {% trans "Check-in Items" %}
  • - {% endif %} {% if owner_control.value == "False" or owner_control.value == "True" and user in owners or user.is_superuser %} - {% if roles.stock.change %} -
    - -
    {% endif %} - {% endif %} + {% endif %} + {% endif %} {% endif %} diff --git a/InvenTree/stock/test_api.py b/InvenTree/stock/test_api.py index c8f6cc77b8..17cab7ffe3 100644 --- a/InvenTree/stock/test_api.py +++ b/InvenTree/stock/test_api.py @@ -7,20 +7,18 @@ from __future__ import unicode_literals from datetime import datetime, timedelta -from rest_framework.test import APITestCase from rest_framework import status from django.urls import reverse -from django.contrib.auth import get_user_model -from InvenTree.helpers import addUserPermissions from InvenTree.status_codes import StockStatus +from InvenTree.api_tester import InvenTreeAPITestCase from common.models import InvenTreeSetting from .models import StockItem, StockLocation -class StockAPITestCase(APITestCase): +class StockAPITestCase(InvenTreeAPITestCase): fixtures = [ 'category', @@ -32,34 +30,16 @@ class StockAPITestCase(APITestCase): 'stock_tests', ] + roles = [ + 'stock.change', + 'stock.add', + 'stock_location.change', + 'stock_location.add', + ] + def setUp(self): - # Create a user for auth - user = get_user_model() - self.user = user.objects.create_user('testuser', 'test@testing.com', 'password') - - self.user.is_staff = True - self.user.save() - - # Add the necessary permissions to the user - perms = [ - 'view_stockitemtestresult', - 'change_stockitemtestresult', - 'add_stockitemtestresult', - 'add_stocklocation', - 'change_stocklocation', - 'add_stockitem', - 'change_stockitem', - ] - - addUserPermissions(self.user, perms) - - self.client.login(username='testuser', password='password') - - def doPost(self, url, data={}): - response = self.client.post(url, data=data, format='json') - - return response + super().setUp() class StockLocationTest(StockAPITestCase): @@ -232,6 +212,9 @@ class StockItemListTest(StockAPITestCase): response = self.get_stock(expired=1) self.assertEqual(len(response), 20) + self.user.is_staff = True + self.user.save() + # Now, ensure that the expiry date feature is enabled! InvenTreeSetting.set_setting('STOCK_ENABLE_EXPIRY', True, self.user) @@ -449,7 +432,7 @@ class StocktakeTest(StockAPITestCase): data = {} # POST with a valid action - response = self.doPost(url, data) + response = self.post(url, data) self.assertContains(response, "must contain list", status_code=status.HTTP_400_BAD_REQUEST) data['items'] = [{ @@ -457,7 +440,7 @@ class StocktakeTest(StockAPITestCase): }] # POST without a PK - response = self.doPost(url, data) + response = self.post(url, data) self.assertContains(response, 'must contain a valid pk', status_code=status.HTTP_400_BAD_REQUEST) # POST with a PK but no quantity @@ -465,14 +448,14 @@ class StocktakeTest(StockAPITestCase): 'pk': 10 }] - response = self.doPost(url, data) + response = self.post(url, data) self.assertContains(response, 'must contain a valid pk', status_code=status.HTTP_400_BAD_REQUEST) data['items'] = [{ 'pk': 1234 }] - response = self.doPost(url, data) + response = self.post(url, data) self.assertContains(response, 'must contain a valid quantity', status_code=status.HTTP_400_BAD_REQUEST) data['items'] = [{ @@ -480,7 +463,7 @@ class StocktakeTest(StockAPITestCase): 'quantity': '10x0d' }] - response = self.doPost(url, data) + response = self.post(url, data) self.assertContains(response, 'must contain a valid quantity', status_code=status.HTTP_400_BAD_REQUEST) data['items'] = [{ @@ -488,7 +471,7 @@ class StocktakeTest(StockAPITestCase): 'quantity': "-1.234" }] - response = self.doPost(url, data) + response = self.post(url, data) self.assertContains(response, 'must not be less than zero', status_code=status.HTTP_400_BAD_REQUEST) # Test with a single item @@ -499,7 +482,7 @@ class StocktakeTest(StockAPITestCase): } } - response = self.doPost(url, data) + response = self.post(url, data) self.assertEqual(response.status_code, status.HTTP_200_OK) def test_transfer(self): @@ -518,13 +501,13 @@ class StocktakeTest(StockAPITestCase): url = reverse('api-stock-transfer') - response = self.doPost(url, data) + response = self.post(url, data) self.assertContains(response, "Moved 1 parts to", status_code=status.HTTP_200_OK) # Now try one which will fail due to a bad location data['location'] = 'not a location' - response = self.doPost(url, data) + response = self.post(url, data) self.assertContains(response, 'Valid location must be specified', status_code=status.HTTP_400_BAD_REQUEST) diff --git a/InvenTree/users/views.py b/InvenTree/users/api.py similarity index 63% rename from InvenTree/users/views.py rename to InvenTree/users/api.py index 97e5f48355..5447bb5547 100644 --- a/InvenTree/users/views.py +++ b/InvenTree/users/api.py @@ -1,3 +1,6 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + from rest_framework import generics, permissions from django.contrib.auth.models import User from django.core.exceptions import ObjectDoesNotExist @@ -8,6 +11,53 @@ from rest_framework.authtoken.models import Token from rest_framework.response import Response from rest_framework import status +from .models import RuleSet, check_user_role + + +class RoleDetails(APIView): + """ + API endpoint which lists the available role permissions + for the current user + + (Requires authentication) + """ + + permission_classes = [ + permissions.IsAuthenticated + ] + + def get(self, request, *args, **kwargs): + + user = request.user + + roles = {} + + for ruleset in RuleSet.RULESET_CHOICES: + + role, text = ruleset + + permissions = [] + + for permission in RuleSet.RULESET_PERMISSIONS: + if check_user_role(user, role, permission): + + permissions.append(permission) + + if len(permissions) > 0: + roles[role] = permissions + else: + roles[role] = None + + data = { + 'user': user.pk, + 'username': user.username, + 'roles': roles, + 'is_staff': user.is_staff, + 'is_superuser': user.is_superuser, + } + + return Response(data) + class UserDetail(generics.RetrieveAPIView): """ Detail endpoint for a single user """ diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index dfe0acc968..3952c00e91 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -67,15 +67,19 @@ class RuleSet(models.Model): 'part_partparametertemplate', 'part_partparameter', 'part_partrelated', + 'part_partstar', ], 'stock_location': [ 'stock_stocklocation', + 'label_stocklocationlabel', ], 'stock': [ 'stock_stockitem', 'stock_stockitemattachment', 'stock_stockitemtracking', 'stock_stockitemtestresult', + 'report_testreport', + 'label_stockitemlabel', ], 'build': [ 'part_part', @@ -86,6 +90,7 @@ class RuleSet(models.Model): 'build_buildorderattachment', 'stock_stockitem', 'stock_stocklocation', + 'report_buildreport', ], 'purchase_order': [ 'company_company', @@ -115,14 +120,9 @@ class RuleSet(models.Model): 'common_colortheme', 'common_inventreesetting', 'company_contact', - 'label_stockitemlabel', - 'label_stocklocationlabel', 'report_reportasset', 'report_reportsnippet', - 'report_testreport', - 'report_buildreport', 'report_billofmaterialsreport', - 'part_partstar', 'users_owner', # Third-party tables @@ -166,6 +166,26 @@ class RuleSet(models.Model): can_delete = models.BooleanField(verbose_name=_('Delete'), default=False, help_text=_('Permission to delete items')) + @classmethod + def check_table_permission(cls, user, table, permission): + """ + Check if the provided user has the specified permission against the table + """ + + # If the table does *not* require permissions + if table in cls.RULESET_IGNORE: + return True + + # Work out which roles touch the given table + for role in cls.RULESET_NAMES: + if table in cls.RULESET_MODELS[role]: + + if check_user_role(user, role, permission): + return True + + print("failed permission check for", table, permission) + return False + @staticmethod def get_model_permission_string(model, permission): """ diff --git a/InvenTree/users/urls.py b/InvenTree/users/urls.py index 312789b55b..7d8d23883f 100644 --- a/InvenTree/users/urls.py +++ b/InvenTree/users/urls.py @@ -1,11 +1,12 @@ from django.conf.urls import url -from . import views +from . import api user_urls = [ - url(r'^(?P[0-9]+)/?$', views.UserDetail.as_view(), name='user-detail'), + url(r'^(?P[0-9]+)/?$', api.UserDetail.as_view(), name='user-detail'), - url(r'token', views.GetAuthToken.as_view(), name='api-token'), + url(r'roles', api.RoleDetails.as_view(), name='api-user-roles'), + url(r'token', api.GetAuthToken.as_view(), name='api-token'), - url(r'^$', views.UserList.as_view()), + url(r'^$', api.UserList.as_view()), ]