Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
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 @@ -107,6 +107,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 @@ -1008,6 +1022,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 @@ -31,6 +31,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 @@ -45,6 +47,8 @@ class SettingsKeyType(TypedDict, total=False):
protected: bool
required: bool
model: str
confirm: bool
confirm_text: str


class InvenTreeSettingsKeyType(SettingsKeyType):
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,9 @@ export function SecurityContent() {
<StylishText size='lg'>{t`Access Tokens`}</StylishText>
</Accordion.Control>
<Accordion.Panel>
<ApiTokenTable only_myself />
<ErrorBoundary fallback={DefaultFallback} onError={onError}>
<ApiTokenTable only_myself />
</ErrorBoundary>
</Accordion.Panel>
</Accordion.Item>
{user.isSuperuser() && (
Expand Down
Loading