Skip to content

Rulset simplification #9506

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 39 additions & 11 deletions src/backend/InvenTree/InvenTree/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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
Expand 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):
Expand Down Expand Up @@ -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
"""
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
46 changes: 6 additions & 40 deletions src/backend/InvenTree/part/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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')

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -2975,7 +2941,7 @@ class PartMetadataAPITest(InvenTreeAPITestCase):
'stock',
]

roles = ['part.change', 'part_category.change']
roles = ['part.change']

def setUp(self):
"""Setup unit tets."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
11 changes: 2 additions & 9 deletions src/backend/InvenTree/stock/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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."""
Expand Down
14 changes: 4 additions & 10 deletions src/backend/InvenTree/users/ruleset.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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')),
Expand Down Expand Up @@ -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',
Expand All @@ -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',
Expand Down
4 changes: 2 additions & 2 deletions src/frontend/src/components/nav/SearchDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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')
},
{
Expand All @@ -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')
},
{
Expand Down
6 changes: 0 additions & 6 deletions src/frontend/src/enums/Roles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}

Expand All @@ -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:
Expand All @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ export default function AdminCenter() {
label: t`Category Parameters`,
icon: <IconSitemap />,
content: <PartCategoryTemplateTable />,
hidden: !user.hasViewRole(UserRoles.part_category)
hidden: !user.hasViewRole(UserRoles.part)
},
{
name: 'stocktake',
Expand All @@ -206,7 +206,7 @@ export default function AdminCenter() {
label: t`Location Types`,
icon: <IconPackages />,
content: <LocationTypesTable />,
hidden: !user.hasViewRole(UserRoles.stock_location)
hidden: !user.hasViewRole(UserRoles.stock)
},
{
name: 'plugin',
Expand Down
6 changes: 3 additions & 3 deletions src/frontend/src/pages/part/CategoryDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})
Expand Down Expand Up @@ -327,7 +327,7 @@ export default function CategoryDetail() {
<InstanceDetail
status={requestStatus}
loading={id ? instanceQuery.isFetching : false}
requiredRole={UserRoles.part_category}
requiredRole={UserRoles.part}
>
<Stack gap='xs'>
<LoadingOverlay visible={instanceQuery.isFetching} />
Expand Down
6 changes: 2 additions & 4 deletions src/frontend/src/pages/part/PartDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -995,7 +995,7 @@ export default function PartDetail() {
requiredRole={UserRoles.part}
>
<Stack gap='xs'>
{user.hasViewRole(UserRoles.part_category) && (
{user.hasViewRole(UserRoles.part) && (
<NavigationTree
title={t`Part Categories`}
modelType={ModelType.partcategory}
Expand All @@ -1018,9 +1018,7 @@ export default function PartDetail() {
imageUrl={part.image}
badges={badges}
breadcrumbs={
user.hasViewRole(UserRoles.part_category)
? breadcrumbs
: undefined
user.hasViewRole(UserRoles.part) ? breadcrumbs : undefined
}
lastCrumb={[
{
Expand Down
Loading
Loading