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:
@@ -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]
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user