2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-04-04 18:40:55 +00:00

fix: low-privilege user token creation (#11492)

* [bug] Users cannot create their own API tokens
Fixes #11486

* fix detection of metadata

* make easier to read

* add handler for IsAuthenticated

* use correct method

* fix style see #11487

* add frontend test

* make test more reliable?
This commit is contained in:
Matthias Mair
2026-03-14 02:02:49 +01:00
committed by GitHub
parent 6deed010a3
commit fda3204e33
4 changed files with 72 additions and 35 deletions

View File

@@ -5,7 +5,7 @@ from django.http import Http404
from django.urls import reverse from django.urls import reverse
import structlog import structlog
from rest_framework import exceptions, serializers from rest_framework import exceptions, permissions, serializers
from rest_framework.fields import empty from rest_framework.fields import empty
from rest_framework.metadata import SimpleMetadata from rest_framework.metadata import SimpleMetadata
from rest_framework.request import clone_request from rest_framework.request import clone_request
@@ -131,10 +131,26 @@ class InvenTreeMetadata(SimpleMetadata):
# Remove any HTTP methods that the user does not have permission for # Remove any HTTP methods that the user does not have permission for
for method, permission in rolemap.items(): for method, permission in rolemap.items():
# general model / role permission
result = check_user_permission(user, self.model, permission) or ( result = check_user_permission(user, self.model, permission) or (
role_required and check_user_role(user, role_required, permission) role_required and check_user_role(user, role_required, permission)
) )
# check if simple IsAuthenticated permission class is used
if not result:
result = (
view.permission_classes
and len(view.permission_classes) == 1
and any(
perm
in [
permissions.IsAuthenticated,
InvenTree.permissions.IsAuthenticatedOrReadScope,
]
for perm in view.permission_classes
)
)
if method in actions and not result: if method in actions and not result:
del actions[method] del actions[method]

View File

@@ -367,49 +367,44 @@ class GetAuthToken(GenericAPIView):
- Existing tokens are *never* exposed again via the API - Existing tokens are *never* exposed again via the API
- Once the token is provided, it can be used for auth until it expires - Once the token is provided, it can be used for auth until it expires
""" """
if request.user.is_authenticated: if not request.user.is_authenticated:
user = request.user raise exceptions.NotAuthenticated() # pragma: no cover
name = request.query_params.get('name', '')
name = ApiToken.sanitize_name(name) user = request.user
name = request.query_params.get('name', '')
today = datetime.date.today() name = ApiToken.sanitize_name(name)
# Find existing token, which has not expired today = datetime.date.today()
token = ApiToken.objects.filter(
user=user, name=name, revoked=False, expiry__gte=today
).first()
if not token: # Find existing token, which has not expired
# User is authenticated, and requesting a token against the provided name. token = ApiToken.objects.filter(
token = ApiToken.objects.create(user=request.user, name=name) user=user, name=name, revoked=False, expiry__gte=today
).first()
logger.info( if not token:
"Created new API token for user '%s' (name='%s')", # User is authenticated, and requesting a token against the provided name.
user.username, token = ApiToken.objects.create(user=request.user, name=name)
name,
) logger.info(
"Created new API token for user '%s' (name='%s')", user.username, name
)
# Add some metadata about the request # Add some metadata about the request
token.set_metadata('user_agent', request.headers.get('user-agent', '')) token.set_metadata('user_agent', request.headers.get('user-agent', ''))
token.set_metadata('remote_addr', request.META.get('REMOTE_ADDR', '')) token.set_metadata('remote_addr', request.META.get('REMOTE_ADDR', ''))
token.set_metadata('remote_host', request.META.get('REMOTE_HOST', '')) token.set_metadata('remote_host', request.META.get('REMOTE_HOST', ''))
token.set_metadata('remote_user', request.META.get('REMOTE_USER', '')) token.set_metadata('remote_user', request.META.get('REMOTE_USER', ''))
token.set_metadata('server_name', request.META.get('SERVER_NAME', '')) token.set_metadata('server_name', request.META.get('SERVER_NAME', ''))
token.set_metadata('server_port', request.META.get('SERVER_PORT', '')) token.set_metadata('server_port', request.META.get('SERVER_PORT', ''))
data = {'token': token.key, 'name': token.name, 'expiry': token.expiry} data = {'token': token.key, 'name': token.name, 'expiry': token.expiry}
# Ensure that the users session is logged in # Ensure that the users session is logged in
if not get_user(request).is_authenticated: if not get_user(request).is_authenticated:
login( login(request, user, backend='django.contrib.auth.backends.ModelBackend')
request, user, backend='django.contrib.auth.backends.ModelBackend'
)
return Response(data) return Response(data)
else:
raise exceptions.NotAuthenticated() # pragma: no cover
class TokenMixin: class TokenMixin:

View File

@@ -26,7 +26,8 @@ export function ApiTokenTable({
const [opened, { open, close }] = useDisclosure(false); const [opened, { open, close }] = useDisclosure(false);
const generateToken = useCreateApiFormModal({ const generateToken = useCreateApiFormModal({
url: ApiEndpoints.user_tokens, url: ApiEndpoints.user_token,
method: 'GET',
title: t`Generate Token`, title: t`Generate Token`,
fields: { name: {} }, fields: { name: {} },
successMessage: t`Token generated`, successMessage: t`Token generated`,
@@ -178,6 +179,7 @@ export function ApiTokenTable({
onClose={close} onClose={close}
title={<StylishText size='xl'>{t`Token`}</StylishText>} title={<StylishText size='xl'>{t`Token`}</StylishText>}
centered centered
data-testid='generated-api-token'
> >
<Text c='dimmed'> <Text c='dimmed'>
<Trans> <Trans>

View File

@@ -543,3 +543,27 @@ async function testColorPicker(page, ref: string) {
await page.mouse.click(box.x + box.width / 2, box.y + box.height + 25); await page.mouse.click(box.x + box.width / 2, box.y + box.height + 25);
await page.getByText('Color Mode').click(); await page.getByText('Color Mode').click();
} }
test('Settings - Auth - Tokens', async ({ browser }) => {
const page = await doCachedLogin(browser, {
username: 'allaccess',
password: 'nolimits',
url: 'settings/user/'
});
await page.getByRole('tab', { name: 'Security' }).click();
await page.getByRole('button', { name: 'Access Tokens' }).click();
await page
.getByRole('button', { name: 'action-button-generate-token' })
.click();
await page
.getByRole('textbox', { name: 'text-field-name' })
.fill('testtoken');
await page.getByRole('button', { name: 'Submit', exact: true }).click();
await page.getByText('Tokens are only shown once').waitFor();
await page
.getByTestId('generated-api-token')
.locator('.mantine-CloseButton-root')
.click();
await page.getByRole('cell', { name: 'testtoken' }).waitFor();
});