Skip to content
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

[PUI] User settings panel updates #7944

Merged
merged 32 commits into from
Sep 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
d5431f5
Simplify user theme settings
SchrodingersGat Aug 21, 2024
dd1d5dc
Cleanup
SchrodingersGat Aug 21, 2024
c83feb1
Fix permission on user list endpoint
SchrodingersGat Aug 21, 2024
cae3c50
Update AccountDetailPanel to use modal form
SchrodingersGat Aug 21, 2024
0d06ffd
Update components
SchrodingersGat Aug 21, 2024
4e27371
Merge branch 'master' of github.com:inventree/InvenTree into theme-si…
SchrodingersGat Aug 21, 2024
b9f8604
UI updates
SchrodingersGat Aug 22, 2024
a8ff676
Implement default colors
SchrodingersGat Aug 22, 2024
19f25f8
Display more user details (read only)
SchrodingersGat Aug 22, 2024
16f56ea
Add specific "MeUserSerializer"
SchrodingersGat Aug 22, 2024
fc36f0b
Add <YesNoUndefinedButton>
SchrodingersGat Aug 22, 2024
682dbc8
Allow role checks to be bypassed for a given view
SchrodingersGat Aug 22, 2024
1657595
Enable 'GET' metadata
SchrodingersGat Aug 22, 2024
f3acd22
Add info on new user account
SchrodingersGat Aug 22, 2024
d02b702
Merge branch 'master' into theme-simplificatio
SchrodingersGat Aug 23, 2024
aa24235
Merge branch 'master' into theme-simplificatio
SchrodingersGat Aug 25, 2024
937f797
Merge branch 'master' into theme-simplificatio
SchrodingersGat Sep 5, 2024
14487ad
Fix boolean expression wrapper
SchrodingersGat Sep 5, 2024
8d4b2d0
Merge branch 'master' into theme-simplificatio
SchrodingersGat Sep 5, 2024
792dcaa
Ruff fixes
SchrodingersGat Sep 5, 2024
2c8ef50
Adjust icon
SchrodingersGat Sep 5, 2024
fe18d78
Merge branch 'master' into theme-simplificatio
SchrodingersGat Sep 5, 2024
8071ded
Update unit test
SchrodingersGat Sep 6, 2024
19dd6ee
Merge branch 'master' into theme-simplificatio
SchrodingersGat Sep 6, 2024
55a2d02
Bummp API version
SchrodingersGat Sep 6, 2024
698ca9f
Merge branch 'master' into theme-simplificatio
SchrodingersGat Sep 6, 2024
467c9c1
Merge remote-tracking branch 'origin/master' into theme-simplificatio
SchrodingersGat Sep 6, 2024
fb97e2d
Merge branch 'theme-simplificatio' of github.com:SchrodingersGat/Inve…
SchrodingersGat Sep 6, 2024
e23f4d1
Merge remote-tracking branch 'origin/master' into theme-simplificatio
SchrodingersGat Sep 9, 2024
61a0acf
Merge branch 'master' into theme-simplificatio
SchrodingersGat Sep 10, 2024
a2f7dc3
Merge branch 'master' into theme-simplificatio
SchrodingersGat Sep 14, 2024
bbcd6b4
Table layout fix
SchrodingersGat Sep 14, 2024
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
1 change: 1 addition & 0 deletions docs/docs/demo.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ The demo instance has a number of user accounts which you can use to explore the

| Username | Password | Staff Access | Enabled | Description |
| -------- | -------- | ------------ | ------- | ----------- |
| noaccess | youshallnotpass | No | Yes | Can login, but has no permissions |
| allaccess | nolimits | No | Yes | View / create / edit all pages and items |
| reader | readonly | No | Yes | Can view all pages but cannot create, edit or delete database records |
| engineer | partsonly | No | Yes | Can manage parts, view stock, but no access to purchase orders or sales orders |
Expand Down
7 changes: 5 additions & 2 deletions src/backend/InvenTree/InvenTree/api_version.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
"""InvenTree API version information."""

# InvenTree API version
INVENTREE_API_VERSION = 252
INVENTREE_API_VERSION = 253

"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""


INVENTREE_API_TEXT = """

v252 - 2024-09-30 : https://github.com/inventree/InvenTree/pull/8035
v253 - 2024-09-14 : https://github.com/inventree/InvenTree/pull/7944
- Adjustments for user API endpoints

v252 - 2024-09-13 : https://github.com/inventree/InvenTree/pull/8040
- Add endpoint for listing all known units

v251 - 2024-09-06 : https://github.com/inventree/InvenTree/pull/8018
Expand Down
45 changes: 40 additions & 5 deletions src/backend/InvenTree/InvenTree/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@

import logging

from rest_framework import serializers
from django.core.exceptions import PermissionDenied
from django.http import Http404

from rest_framework import exceptions, serializers
from rest_framework.fields import empty
from rest_framework.metadata import SimpleMetadata
from rest_framework.request import clone_request
from rest_framework.utils import model_meta

import common.models
Expand All @@ -29,6 +33,40 @@ class InvenTreeMetadata(SimpleMetadata):
so we can perform lookup for ForeignKey related fields.
"""

def determine_actions(self, request, view):
"""Determine the 'actions' available to the user for the given view.

Note that this differs from the standard DRF implementation,
in that we also allow annotation for the 'GET' method.

This allows the client to determine what fields are available,
even if they are only for a read (GET) operation.

See SimpleMetadata.determine_actions for more information.
"""
actions = {}

for method in {'PUT', 'POST', 'GET'} & set(view.allowed_methods):
view.request = clone_request(request, method)
try:
# Test global permissions
if hasattr(view, 'check_permissions'):
view.check_permissions(view.request)
# Test object permissions
if method == 'PUT' and hasattr(view, 'get_object'):
view.get_object()
except (exceptions.APIException, PermissionDenied, Http404):
pass
else:
# If user has appropriate permissions for the view, include
# appropriate metadata about the fields that should be supplied.
serializer = view.get_serializer()
actions[method] = self.get_serializer_info(serializer)
finally:
view.request = request

return actions

def determine_metadata(self, request, view):
"""Overwrite the metadata to adapt to the request user."""
self.request = request
Expand Down Expand Up @@ -81,6 +119,7 @@ def determine_metadata(self, request, view):

# Map the request method to a permission type
rolemap = {
'GET': 'view',
'POST': 'add',
'PUT': 'change',
'PATCH': 'change',
Expand All @@ -102,10 +141,6 @@ def determine_metadata(self, request, view):
if 'DELETE' in view.allowed_methods and check(user, table, 'delete'):
actions['DELETE'] = {}

# Add a 'VIEW' action if we are allowed to view
if 'GET' in view.allowed_methods and check(user, table, 'view'):
actions['GET'] = {}

metadata['actions'] = actions

except AttributeError:
Expand Down
14 changes: 14 additions & 0 deletions src/backend/InvenTree/InvenTree/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ def has_permission(self, request, view):
# Extract the model name associated with this request
model = get_model_for_view(view)

if model is None:
return True

app_label = model._meta.app_label
model_name = model._meta.model_name

Expand All @@ -99,6 +102,17 @@ def has_permission(self, request, view):
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."""
return bool(
(request.user and request.user.is_superuser)
or request.method in permissions.SAFE_METHODS
)


class IsStaffOrReadOnly(permissions.IsAdminUser):
"""Allows read-only access to any user, but write access is restricted to staff users."""

Expand Down
33 changes: 31 additions & 2 deletions src/backend/InvenTree/InvenTree/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -403,18 +403,21 @@ class Meta:
read_only_fields = ['username', 'email']

username = serializers.CharField(label=_('Username'), help_text=_('Username'))

first_name = serializers.CharField(
label=_('First Name'), help_text=_('First name of the user'), allow_blank=True
)

last_name = serializers.CharField(
label=_('Last Name'), help_text=_('Last name of the user'), allow_blank=True
)

email = serializers.EmailField(
label=_('Email'), help_text=_('Email address of the user'), allow_blank=True
)


class ExendedUserSerializer(UserSerializer):
class ExtendedUserSerializer(UserSerializer):
"""Serializer for a User with a bit more info."""

from users.serializers import GroupSerializer
Expand All @@ -437,9 +440,11 @@ class Meta(UserSerializer.Meta):
is_staff = serializers.BooleanField(
label=_('Staff'), help_text=_('Does this user have staff permissions')
)

is_superuser = serializers.BooleanField(
label=_('Superuser'), help_text=_('Is this user a superuser')
)

is_active = serializers.BooleanField(
label=_('Active'), help_text=_('Is this user account active')
)
Expand All @@ -464,9 +469,33 @@ def validate(self, attrs):
return super().validate(attrs)


class UserCreateSerializer(ExendedUserSerializer):
class MeUserSerializer(ExtendedUserSerializer):
"""API serializer specifically for the 'me' endpoint."""

class Meta(ExtendedUserSerializer.Meta):
"""Metaclass options.

Extends the ExtendedUserSerializer.Meta options,
but ensures that certain fields are read-only.
"""

read_only_fields = [
*ExtendedUserSerializer.Meta.read_only_fields,
'is_active',
'is_staff',
'is_superuser',
]


class UserCreateSerializer(ExtendedUserSerializer):
"""Serializer for creating a new User."""

class Meta(ExtendedUserSerializer.Meta):
"""Metaclass options for the UserCreateSerializer."""

# Prevent creation of users with superuser or staff permissions
read_only_fields = ['groups', 'is_staff', 'is_superuser']

def validate(self, attrs):
"""Expanded valiadation for auth."""
# Check that the user trying to create a new user is a superuser
Expand Down
25 changes: 22 additions & 3 deletions src/backend/InvenTree/users/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from rest_framework.views import APIView

import InvenTree.helpers
import InvenTree.permissions
from common.models import InvenTreeSetting
from InvenTree.filters import SEARCH_ORDER_FILTER
from InvenTree.mixins import (
Expand All @@ -33,7 +34,11 @@
RetrieveUpdateAPI,
RetrieveUpdateDestroyAPI,
)
from InvenTree.serializers import ExendedUserSerializer, UserCreateSerializer
from InvenTree.serializers import (
ExtendedUserSerializer,
MeUserSerializer,
UserCreateSerializer,
)
from InvenTree.settings import FRONTEND_URL_BASE
from users.models import ApiToken, Owner
from users.serializers import (
Expand Down Expand Up @@ -134,24 +139,38 @@ class UserDetail(RetrieveUpdateDestroyAPI):
"""Detail endpoint for a single user."""

queryset = User.objects.all()
serializer_class = ExendedUserSerializer
serializer_class = ExtendedUserSerializer
permission_classes = [permissions.IsAuthenticated]


class MeUserDetail(RetrieveUpdateAPI, UserDetail):
"""Detail endpoint for current user."""

serializer_class = MeUserSerializer

rolemap = {'POST': 'view', 'PUT': 'view', 'PATCH': 'view'}

def get_object(self):
"""Always return the current user object."""
return self.request.user

def get_permission_model(self):
"""Return the model for the permission check.

Note that for this endpoint, the current user can *always* edit their own details.
"""
return None


class UserList(ListCreateAPI):
"""List endpoint for detail on all users."""

queryset = User.objects.all()
serializer_class = UserCreateSerializer
permission_classes = [permissions.IsAuthenticated]
permission_classes = [
permissions.IsAuthenticated,
InvenTree.permissions.IsSuperuserOrReadOnly,
]
filter_backends = SEARCH_ORDER_FILTER

search_fields = ['first_name', 'last_name', 'username']
Expand Down
5 changes: 4 additions & 1 deletion src/backend/InvenTree/users/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ def test_user_options(self):
self.assignRole('admin.add')
response = self.options(reverse('api-user-list'), expected_code=200)

fields = response.data['actions']['POST']
# User is *not* a superuser, so user account API is read-only
self.assertNotIn('POST', response.data['actions'])

fields = response.data['actions']['GET']

# Check some of the field values
self.assertEqual(fields['username']['label'], 'Username')
Expand Down
10 changes: 9 additions & 1 deletion src/frontend/src/components/buttons/YesNoButton.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { t } from '@lingui/macro';
import { Badge } from '@mantine/core';
import { Badge, Skeleton } from '@mantine/core';

import { isTrue } from '../../functions/conversion';

Expand Down Expand Up @@ -32,3 +32,11 @@ export function PassFailButton({
export function YesNoButton({ value }: { value: any }) {
return <PassFailButton value={value} passText={t`Yes`} failText={t`No`} />;
}

export function YesNoUndefinedButton({ value }: { value?: boolean }) {
if (value === undefined) {
return <Skeleton height={15} width={32} />;
} else {
return <YesNoButton value={value} />;
}
}
Loading
Loading