2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-03-21 11:44:42 +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
import structlog
from rest_framework import exceptions, serializers
from rest_framework import exceptions, permissions, serializers
from rest_framework.fields import empty
from rest_framework.metadata import SimpleMetadata
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
for method, permission in rolemap.items():
# general model / role permission
result = check_user_permission(user, self.model, permission) or (
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:
del actions[method]

View File

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

View File

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