Skip to content
19 changes: 19 additions & 0 deletions src/backend/InvenTree/common/setting/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,20 @@ def reload_plugin_registry(setting):
registry.reload_plugins(full_reload=True, force_reload=True, collect=True)


def enforce_mfa(setting):
"""Enforce multifactor authentication for all users."""
from allauth.usersessions.models import UserSession

from common.models import logger

logger.info(
'Enforcing multifactor authentication for all users by signing out all sessions.'
)
for session in UserSession.objects.all():
session.end()
logger.info('All user sessions have been ended.')


def barcode_plugins() -> list:
"""Return a list of plugin choices which can be used for barcode generation."""
try:
Expand Down Expand Up @@ -1007,6 +1021,11 @@ class SystemSetId:
'description': _('Users must use multifactor security.'),
'default': False,
'validator': bool,
'confirm': True,
'confirm_text': _(
'Enabling this setting will require all users to set up multifactor authentication. All sessions will be disconnected immediately.'
),
'after_save': enforce_mfa,
},
'PLUGIN_ON_STARTUP': {
'name': _('Check plugins on startup'),
Expand Down
4 changes: 4 additions & 0 deletions src/backend/InvenTree/common/setting/type.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ class SettingsKeyType(TypedDict, total=False):
protected: Protected values are not returned to the client, instead "***" is returned (optional, default: False)
required: Is this setting required to work, can be used in combination with .check_all_settings(...) (optional, default: False)
model: Auto create a dropdown menu to select an associated model instance (e.g. 'company.company', 'auth.user' and 'auth.group' are possible too, optional)
confirm: Require an explicit confirmation before changing the setting (optional, default: False)
confirm_text: Text to display in the confirmation dialog (optional)
"""

name: str
Expand All @@ -46,6 +48,8 @@ class SettingsKeyType(TypedDict, total=False):
protected: bool
required: bool
model: str
confirm: bool
confirm_text: str


class InvenTreeSettingsKeyType(SettingsKeyType):
Expand Down
2 changes: 2 additions & 0 deletions src/backend/InvenTree/common/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,8 @@ def run_settings_check(self, key, setting):
'requires_restart',
'after_save',
'before_save',
'confirm',
'confirm_text',
]

for k in setting:
Expand Down
1 change: 1 addition & 0 deletions src/frontend/lib/enums/ApiEndpoints.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export enum ApiEndpoints {
user_simple_login = 'email/generate/',

// User auth endpoints
auth_base = '/auth/',
user_reset = 'auth/v1/auth/password/request',
user_reset_set = 'auth/v1/auth/password/reset',
auth_pwd_change = 'auth/v1/account/password/change',
Expand Down
4 changes: 3 additions & 1 deletion src/frontend/src/components/Boundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import { ErrorBoundary, type FallbackRender } from '@sentry/react';
import { IconExclamationCircle } from '@tabler/icons-react';
import { type ReactNode, useCallback } from 'react';

function DefaultFallback({ title }: Readonly<{ title: string }>): ReactNode {
export function DefaultFallback({
title
}: Readonly<{ title: string }>): ReactNode {
return (
<Alert
color='red'
Expand Down
26 changes: 25 additions & 1 deletion src/frontend/src/functions/auth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,11 @@ export async function doBasicLogin(
});

if (loginDone) {
await fetchUserState();
// see if mfa registration is required
checkMfaSetup(navigate);

// gather required states
await fetchUserState();
await fetchGlobalStates(navigate);
observeProfile();
} else if (!success) {
Expand Down Expand Up @@ -237,6 +240,23 @@ export const doSimpleLogin = async (email: string) => {
return mail;
};

function checkMfaSetup(navigate?: NavigateFunction) {
api
.get(apiUrl(ApiEndpoints.auth_base))
.then(() => {})
.catch((err) => {
if (err?.response?.status == 401) {
if (navigate != undefined) {
navigate('/mfa-setup');
} else {
alert(
'MFA setup required, but no navigation possible - please reload the website'
);
}
}
});
}

function observeProfile() {
// overwrite language and theme info in session with profile info

Expand Down Expand Up @@ -446,6 +466,10 @@ function handleSuccessFullAuth(
}
setAuthenticated();

// see if mfa registration is required
checkMfaSetup(navigate);

// get required states
fetchUserState().finally(() => {
observeProfile();
fetchGlobalStates(navigate);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
TextInput
} from '@mantine/core';
import { hideNotification, showNotification } from '@mantine/notifications';
import { ErrorBoundary } from '@sentry/react';
import {
IconAlertCircle,
IconAt,
Expand All @@ -29,6 +30,7 @@ import {
import { useQuery } from '@tanstack/react-query';
import { useCallback, useMemo, useState } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { DefaultFallback } from '../../../../components/Boundary';
import { StylishText } from '../../../../components/items/StylishText';
import { ProviderLogin, authApi } from '../../../../functions/auth';
import { useServerApiState } from '../../../../states/ServerApiState';
Expand All @@ -43,6 +45,13 @@ export function SecurityContent() {

const user = useUserState();

const onError = useCallback(
(error: unknown, componentStack: string | undefined, eventId: string) => {
console.error(`ERR: Error rendering component: ${error}`);
},
[]
);

return (
<Stack>
<Accordion multiple defaultValue={['email']}>
Expand Down Expand Up @@ -85,7 +94,12 @@ export function SecurityContent() {
<StylishText size='lg'>{t`Access Tokens`}</StylishText>
</Accordion.Control>
<Accordion.Panel>
<ApiTokenTable only_myself />
<ErrorBoundary
fallback={<DefaultFallback title={'API Table'} />}
onError={onError}
>
<ApiTokenTable only_myself />
</ErrorBoundary>
</Accordion.Panel>
</Accordion.Item>
{user.isSuperuser() && (
Expand Down
Loading