diff --git a/src/backend/InvenTree/InvenTree/permissions.py b/src/backend/InvenTree/InvenTree/permissions.py index e1a5bb6a131b..c15fdb7bf3c6 100644 --- a/src/backend/InvenTree/InvenTree/permissions.py +++ b/src/backend/InvenTree/InvenTree/permissions.py @@ -8,7 +8,11 @@ def get_model_for_view(view): - """Attempt to introspect the 'model' type for an API view.""" + """Attempt to introspect the 'model' type for an API view. + + Arguments: + view: The view being accessed + """ if hasattr(view, 'get_permission_model'): return view.get_permission_model() @@ -43,8 +47,13 @@ class RolePermission(permissions.BasePermission): 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.""" + def has_permission(self, request, view) -> bool: + """Determine if the current user has the specified permissions. + + Arguments: + request: The HTTP request + view: The view being accessed + """ user = request.user # Superuser can do it all @@ -65,7 +74,7 @@ def has_permission(self, request, view): if hasattr(view, 'rolemap'): rolemap.update(view.rolemap) - permission = rolemap[request.method] + permission = rolemap.get(request.method) # The required role may be defined for the view class if role := getattr(view, 'role_required', None): @@ -95,9 +104,13 @@ class RolePermissionOrReadOnly(RolePermission): REQUIRE_STAFF = False - def has_permission(self, request, view): + def has_permission(self, request, view) -> bool: """Determine if the current user has the specified permissions. + Arguments: + request: The HTTP request + view: The view being accessed + - If the user does have the required role, then allow the request - If the user does not have the required role, but is authenticated, then allow read-only access """ @@ -125,16 +138,26 @@ class StaffRolePermissionOrReadOnly(RolePermissionOrReadOnly): class IsSuperuser(permissions.IsAdminUser): """Allows access only to superuser users.""" - def has_permission(self, request, view): - """Check if the user is a superuser.""" + def has_permission(self, request, view) -> bool: + """Check if the user is a superuser. + + Arguments: + request: The HTTP request + view: The view being accessed + """ return bool(request.user and request.user.is_superuser) class IsSuperuserOrReadOnly(permissions.IsAdminUser): """Allow read-only access to any user, but write access is restricted to superuser users.""" - def has_permission(self, request, view): - """Check if the user is a superuser.""" + def has_permission(self, request, view) -> bool: + """Check if the user is a superuser. + + Arguments: + request: The HTTP request + view: The view being accessed + """ return bool( (request.user and request.user.is_superuser) or request.method in permissions.SAFE_METHODS @@ -144,8 +167,13 @@ def has_permission(self, request, view): class IsStaffOrReadOnly(permissions.IsAdminUser): """Allows read-only access to any user, but write access is restricted to staff users.""" - def has_permission(self, request, view): - """Check if the user is a superuser.""" + def has_permission(self, request, view) -> bool: + """Check if the user is a superuser. + + Arguments: + request: The HTTP request + view: The view being accessed + """ return bool( (request.user and request.user.is_staff) or request.method in permissions.SAFE_METHODS diff --git a/src/backend/InvenTree/part/test_api.py b/src/backend/InvenTree/part/test_api.py index 4d8af0584645..c4a655aed1ad 100644 --- a/src/backend/InvenTree/part/test_api.py +++ b/src/backend/InvenTree/part/test_api.py @@ -42,13 +42,7 @@ class PartImageTestMixin: """Mixin for testing part images.""" - roles = [ - 'part.change', - 'part.add', - 'part.delete', - 'part_category.change', - 'part_category.add', - ] + roles = ['part.change', 'part.add', 'part.delete'] @classmethod def setUpTestData(cls): @@ -98,14 +92,7 @@ class PartCategoryAPITest(InvenTreeAPITestCase): 'stock', ] - roles = [ - 'part.change', - 'part.add', - 'part.delete', - 'part_category.change', - 'part_category.add', - 'part_category.delete', - ] + roles = ['part.change', 'part.add', 'part.delete'] def test_category_list(self): """Test the PartCategoryList API endpoint.""" @@ -636,7 +623,7 @@ class PartOptionsAPITest(InvenTreeAPITestCase): Ensure that the required field details are provided! """ - roles = ['part.add', 'part_category.view'] + roles = ['part.add', 'part.view'] def test_part(self): """Test the Part API OPTIONS.""" @@ -719,17 +706,9 @@ def test_part_label_translation(self): def test_category(self): """Test the PartCategory API OPTIONS endpoint.""" - actions = self.getActions(reverse('api-part-category-list')) - - # actions should *not* contain 'POST' as we do not have the correct role - self.assertNotIn('POST', actions) - - self.assignRole('part_category.add') - actions = self.getActions(reverse('api-part-category-list'))['POST'] name = actions['name'] - self.assertTrue(name['required']) self.assertEqual(name['label'], 'Name') @@ -777,13 +756,7 @@ class PartAPITestBase(InvenTreeAPITestCase): 'stock', ] - roles = [ - 'part.change', - 'part.add', - 'part.delete', - 'part_category.change', - 'part_category.add', - ] + roles = ['part.change', 'part.add', 'part.delete'] class PartAPITest(PartAPITestBase): @@ -2750,14 +2723,7 @@ class PartInternalPriceBreakTest(InvenTreeAPITestCase): 'stock', ] - roles = [ - 'part.change', - 'part.add', - 'part.delete', - 'part_category.change', - 'part_category.add', - 'part_category.delete', - ] + roles = ['part.change', 'part.add', 'part.delete'] def test_create_price_breaks(self): """Test we can create price breaks at various quantities.""" @@ -2975,7 +2941,7 @@ class PartMetadataAPITest(InvenTreeAPITestCase): 'stock', ] - roles = ['part.change', 'part_category.change'] + roles = ['part.change'] def setUp(self): """Setup unit tets.""" diff --git a/src/backend/InvenTree/plugin/builtin/barcodes/test_inventree_barcode.py b/src/backend/InvenTree/plugin/builtin/barcodes/test_inventree_barcode.py index 66064d768d76..c1543c60e18d 100644 --- a/src/backend/InvenTree/plugin/builtin/barcodes/test_inventree_barcode.py +++ b/src/backend/InvenTree/plugin/builtin/barcodes/test_inventree_barcode.py @@ -209,7 +209,7 @@ def test_assign_to_location(self): data={'barcode': barcode, 'stocklocation': 1}, expected_code=403 ) - self.assignRole('stock_location.change') + self.assignRole('stock.change') # Assign random barcode data to a StockLocation instance response = self.assign( diff --git a/src/backend/InvenTree/stock/test_api.py b/src/backend/InvenTree/stock/test_api.py index d579edb78fd5..4e6ca294881b 100644 --- a/src/backend/InvenTree/stock/test_api.py +++ b/src/backend/InvenTree/stock/test_api.py @@ -44,14 +44,7 @@ class StockAPITestCase(InvenTreeAPITestCase): 'stock_tests', ] - roles = [ - 'stock.change', - 'stock.add', - 'stock_location.change', - 'stock_location.add', - 'stock_location.delete', - 'stock.delete', - ] + roles = ['stock.change', 'stock.add', 'stock.delete'] class StockLocationTest(StockAPITestCase): @@ -2362,7 +2355,7 @@ class StockMetadataAPITest(InvenTreeAPITestCase): 'stock_tests', ] - roles = ['stock.change', 'stock_location.change'] + roles = ['stock.change'] def metatester(self, apikey, model): """Generic tester.""" diff --git a/src/backend/InvenTree/users/ruleset.py b/src/backend/InvenTree/users/ruleset.py index d35309c41de6..05d686480ba2 100644 --- a/src/backend/InvenTree/users/ruleset.py +++ b/src/backend/InvenTree/users/ruleset.py @@ -14,10 +14,8 @@ def __str__(self): return str(self.value) ADMIN = 'admin' - PART_CATEGORY = 'part_category' PART = 'part' STOCKTAKE = 'stocktake' - STOCK_LOCATION = 'stock_location' STOCK = 'stock' BUILD = 'build' PURCHASE_ORDER = 'purchase_order' @@ -29,10 +27,8 @@ def __str__(self): # These are used to determine the permissions available to a group of users. RULESET_CHOICES = [ (RuleSetEnum.ADMIN, _('Admin')), - (RuleSetEnum.PART_CATEGORY, _('Part Categories')), (RuleSetEnum.PART, _('Parts')), (RuleSetEnum.STOCKTAKE, _('Stocktake')), - (RuleSetEnum.STOCK_LOCATION, _('Stock Locations')), (RuleSetEnum.STOCK, _('Stock Items')), (RuleSetEnum.BUILD, _('Build Orders')), (RuleSetEnum.PURCHASE_ORDER, _('Purchase Orders')), @@ -85,11 +81,6 @@ def get_ruleset_models() -> dict: 'machine_machineconfig', 'machine_machinesetting', ], - RuleSetEnum.PART_CATEGORY: [ - 'part_partcategory', - 'part_partcategoryparametertemplate', - 'part_partcategorystar', - ], RuleSetEnum.PART: [ 'part_part', 'part_partpricing', @@ -102,17 +93,20 @@ def get_ruleset_models() -> dict: 'part_partparameter', 'part_partrelated', 'part_partstar', + 'part_partcategory', 'part_partcategorystar', + 'part_partcategoryparametertemplate', 'company_supplierpart', 'company_manufacturerpart', 'company_manufacturerpartparameter', ], RuleSetEnum.STOCKTAKE: ['part_partstocktake', 'part_partstocktakereport'], - RuleSetEnum.STOCK_LOCATION: ['stock_stocklocation', 'stock_stocklocationtype'], RuleSetEnum.STOCK: [ 'stock_stockitem', 'stock_stockitemtracking', 'stock_stockitemtestresult', + 'stock_stocklocation', + 'stock_stocklocationtype', ], RuleSetEnum.BUILD: [ 'part_part', diff --git a/src/frontend/src/components/nav/SearchDrawer.tsx b/src/frontend/src/components/nav/SearchDrawer.tsx index cc9e0b8c9958..1d853a6950a6 100644 --- a/src/frontend/src/components/nav/SearchDrawer.tsx +++ b/src/frontend/src/components/nav/SearchDrawer.tsx @@ -248,7 +248,7 @@ export function SearchDrawer({ model: ModelType.partcategory, parameters: {}, enabled: - user.hasViewRole(UserRoles.part_category) && + user.hasViewRole(UserRoles.part) && userSettings.isSet('SEARCH_PREVIEW_SHOW_CATEGORIES') }, { @@ -268,7 +268,7 @@ export function SearchDrawer({ model: ModelType.stocklocation, parameters: {}, enabled: - user.hasViewRole(UserRoles.stock_location) && + user.hasViewRole(UserRoles.stock) && userSettings.isSet('SEARCH_PREVIEW_SHOW_LOCATIONS') }, { diff --git a/src/frontend/src/enums/Roles.tsx b/src/frontend/src/enums/Roles.tsx index 4c2d3bbaa8d1..2169473b8d2d 100644 --- a/src/frontend/src/enums/Roles.tsx +++ b/src/frontend/src/enums/Roles.tsx @@ -7,12 +7,10 @@ export enum UserRoles { admin = 'admin', build = 'build', part = 'part', - part_category = 'part_category', purchase_order = 'purchase_order', return_order = 'return_order', sales_order = 'sales_order', stock = 'stock', - stock_location = 'stock_location', stocktake = 'stocktake' } @@ -34,8 +32,6 @@ export function userRoleLabel(role: UserRoles): string { return t`Build Orders`; case UserRoles.part: return t`Parts`; - case UserRoles.part_category: - return t`Part Categories`; case UserRoles.purchase_order: return t`Purchase Orders`; case UserRoles.return_order: @@ -44,8 +40,6 @@ export function userRoleLabel(role: UserRoles): string { return t`Sales Orders`; case UserRoles.stock: return t`Stock Items`; - case UserRoles.stock_location: - return t`Stock Location`; case UserRoles.stocktake: return t`Stocktake`; default: diff --git a/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx b/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx index f0deb5cab62f..d9b11c5d1f42 100644 --- a/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx +++ b/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx @@ -180,7 +180,7 @@ export default function AdminCenter() { label: t`Category Parameters`, icon: , content: , - hidden: !user.hasViewRole(UserRoles.part_category) + hidden: !user.hasViewRole(UserRoles.part) }, { name: 'stocktake', @@ -206,7 +206,7 @@ export default function AdminCenter() { label: t`Location Types`, icon: , content: , - hidden: !user.hasViewRole(UserRoles.stock_location) + hidden: !user.hasViewRole(UserRoles.stock) }, { name: 'plugin', diff --git a/src/frontend/src/pages/part/CategoryDetail.tsx b/src/frontend/src/pages/part/CategoryDetail.tsx index 289c0a405203..12221c397334 100644 --- a/src/frontend/src/pages/part/CategoryDetail.tsx +++ b/src/frontend/src/pages/part/CategoryDetail.tsx @@ -241,12 +241,12 @@ export default function CategoryDetail() { tooltip={t`Category Actions`} actions={[ EditItemAction({ - hidden: !id || !user.hasChangeRole(UserRoles.part_category), + hidden: !id || !user.hasChangeRole(UserRoles.part), tooltip: t`Edit Part Category`, onClick: () => editCategory.open() }), DeleteItemAction({ - hidden: !id || !user.hasDeleteRole(UserRoles.part_category), + hidden: !id || !user.hasDeleteRole(UserRoles.part), tooltip: t`Delete Part Category`, onClick: () => deleteCategory.open() }) @@ -327,7 +327,7 @@ export default function CategoryDetail() { diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx index 9a4cf99740d9..d6e23c1c9dda 100644 --- a/src/frontend/src/pages/part/PartDetail.tsx +++ b/src/frontend/src/pages/part/PartDetail.tsx @@ -995,7 +995,7 @@ export default function PartDetail() { requiredRole={UserRoles.part} > - {user.hasViewRole(UserRoles.part_category) && ( + {user.hasViewRole(UserRoles.part) && ( editLocation.open() }), DeleteItemAction({ - hidden: !id || !user.hasDeleteRole(UserRoles.stock_location), + hidden: !id || !user.hasDeleteRole(UserRoles.stock), tooltip: t`Delete Stock Location`, onClick: () => deleteLocation.open() }) @@ -365,7 +365,7 @@ export default function Stock() { - {user.hasViewRole(UserRoles.stock_location) && ( + {user.hasViewRole(UserRoles.stock) && ( ) { }); const tableActions = useMemo(() => { - const can_add = user.hasAddRole(UserRoles.part_category); - const can_edit = user.hasChangeRole(UserRoles.part_category); + const can_add = user.hasAddRole(UserRoles.part); + const can_edit = user.hasChangeRole(UserRoles.part); return [ ) { const rowActions = useCallback( (record: any): RowAction[] => { - const can_edit = user.hasChangeRole(UserRoles.part_category); + const can_edit = user.hasChangeRole(UserRoles.part); return [ RowEditAction({ diff --git a/src/frontend/src/tables/stock/LocationTypesTable.tsx b/src/frontend/src/tables/stock/LocationTypesTable.tsx index 632a1741bd5a..84863125ae81 100644 --- a/src/frontend/src/tables/stock/LocationTypesTable.tsx +++ b/src/frontend/src/tables/stock/LocationTypesTable.tsx @@ -84,14 +84,14 @@ export default function LocationTypesTable() { (record: any): RowAction[] => { return [ RowEditAction({ - hidden: !user.hasChangeRole(UserRoles.stock_location), + hidden: !user.hasChangeRole(UserRoles.stock), onClick: () => { setSelectedLocationType(record.pk); editLocationType.open(); } }), RowDeleteAction({ - hidden: !user.hasDeleteRole(UserRoles.stock_location), + hidden: !user.hasDeleteRole(UserRoles.stock), onClick: () => { setSelectedLocationType(record.pk); deleteLocationType.open(); @@ -108,7 +108,7 @@ export default function LocationTypesTable() { key='add-location-type' tooltip={t`Add Location Type`} onClick={() => newLocationType.open()} - hidden={!user.hasAddRole(UserRoles.stock_location)} + hidden={!user.hasAddRole(UserRoles.stock)} /> ]; }, [user]); diff --git a/src/frontend/src/tables/stock/StockLocationTable.tsx b/src/frontend/src/tables/stock/StockLocationTable.tsx index 20c7275f2315..f49a4bada218 100644 --- a/src/frontend/src/tables/stock/StockLocationTable.tsx +++ b/src/frontend/src/tables/stock/StockLocationTable.tsx @@ -132,8 +132,8 @@ export function StockLocationTable({ parentId }: Readonly<{ parentId?: any }>) { }); const tableActions = useMemo(() => { - const can_add = user.hasAddRole(UserRoles.stock_location); - const can_edit = user.hasChangeRole(UserRoles.stock_location); + const can_add = user.hasAddRole(UserRoles.stock); + const can_edit = user.hasChangeRole(UserRoles.stock); return [ ) { const rowActions = useCallback( (record: any): RowAction[] => { - const can_edit = user.hasChangeRole(UserRoles.stock_location); + const can_edit = user.hasChangeRole(UserRoles.stock); return [ RowEditAction({