mirror of
https://github.com/inventree/InvenTree.git
synced 2026-04-06 11:31:04 +00:00
feat(frontend): improve comms around danger of staff users (#11659)
* docs: add more details around staff / superuser roles and their dangers * make clear that staff users are dangerous * make distinction clearer in API * add error code and frontend warning about running with staff / admin user * fix test * bump api * adapt banner warning * make banner locally disableable * add global option to disable elevated user alert
This commit is contained in:
@@ -210,6 +210,12 @@ The environment in which the backup was taken does not match the current environ
|
||||
|
||||
This warning will not prevent you from restoring the backup but it is recommended to ensure the mentioned issues are resolved before restoring the backup to prevent issues with the restored instance.
|
||||
|
||||
#### INVE-W14
|
||||
**Elevated privileges - Backend**
|
||||
|
||||
A user is logged in with elevated privileges. This might be a superuser or a administrator user. These types of users have elevated permissions and should not be used for regular usage.
|
||||
Use separate accounts for administrative tasks and regular usage to reduce risk. Make sure to review the [permission documentation](../settings/permissions.md#dangerous-user-flags).
|
||||
|
||||
|
||||
### INVE-I (InvenTree Information)
|
||||
Information — These are not errors but information messages. They might point out potential issues or just provide information.
|
||||
|
||||
@@ -535,6 +535,7 @@ Set the `INVENTREE_FRONTEND_SETTINGS` Environment variable to a JSON object or u
|
||||
| `url_compatibility` | Support compatibility with "legacy" URLs? | `true` |
|
||||
| `sentry_dsn` | Set a Sentry DSN url | *Not specified* |
|
||||
| `mobile_mode` | Controls if InvenTree web UI can be used by mobile devices. There are 3 options: `default` - does not allow mobile devices; `allow-ignore` - shows a mobile device detected banner with a button to ignore this warning AT THE USERS OWN RISK; `allow-always` - skips the mobile check and allows mobile devices always (of course at the server admins OWN RISK) | `default` |
|
||||
| `dangerous_hide_evelevated_alert` | Hides the elevated permissions alert in the UI. This is a dangerous option as using the UI with elevated permissions is against the threat model. | `false` |
|
||||
|
||||
E.g. to allow mobile devices to ignore the mobile check, use the following Environment variable:
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
"""InvenTree API version information."""
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 469
|
||||
INVENTREE_API_VERSION = 470
|
||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||
|
||||
INVENTREE_API_TEXT = """
|
||||
|
||||
v470 -> 2026-04-01 : https://github.com/inventree/InvenTree/pull/11659
|
||||
- Renames "is_staff" field to "is_admin" and updates help texts accordingly to highlight current security boundaries
|
||||
|
||||
v469 -> 2026-03-31 : https://github.com/inventree/InvenTree/pull/11641
|
||||
- Adds parameter support to the SalesOrderShipment model and API endpoints
|
||||
|
||||
|
||||
@@ -306,8 +306,8 @@ class ExtendedUserSerializer(UserSerializer):
|
||||
)
|
||||
|
||||
is_staff = serializers.BooleanField(
|
||||
label=_('Staff'),
|
||||
help_text=_('Does this user have staff permissions'),
|
||||
label=_('Administrator'),
|
||||
help_text=_('Does this user have administrative permissions'),
|
||||
required=False,
|
||||
)
|
||||
|
||||
|
||||
@@ -37,9 +37,10 @@ class UserAPITests(InvenTreeAPITestCase):
|
||||
fields['is_active']['help_text'], 'Is this user account active'
|
||||
)
|
||||
|
||||
self.assertEqual(fields['is_staff']['label'], 'Staff')
|
||||
self.assertEqual(fields['is_staff']['label'], 'Administrator')
|
||||
self.assertEqual(
|
||||
fields['is_staff']['help_text'], 'Does this user have staff permissions'
|
||||
fields['is_staff']['help_text'],
|
||||
'Does this user have administrative permissions',
|
||||
)
|
||||
|
||||
def test_api_url(self):
|
||||
|
||||
@@ -127,7 +127,7 @@ function HoverNameBadge(data: any, type: BadgeType) {
|
||||
data?.image,
|
||||
<>
|
||||
{data.is_superuser && <Badge color='red'>{t`Superuser`}</Badge>}
|
||||
{data.is_staff && <Badge color='blue'>{t`Staff`}</Badge>}
|
||||
{data.is_staff && <Badge color='orange'>{t`Administrator`}</Badge>}
|
||||
{data.email && t`Email: ` + data.email}
|
||||
</>
|
||||
];
|
||||
|
||||
@@ -7,5 +7,7 @@ export const OnlyStaff = ({ children }: { children: any }) => {
|
||||
const [user] = useUserState(useShallow((state) => [state.user]));
|
||||
|
||||
if (user?.is_staff) return children;
|
||||
return <Trans>This information is only available for staff users</Trans>;
|
||||
return (
|
||||
<Trans>This information is only available for administrative users</Trans>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,8 +8,7 @@ import {
|
||||
Group,
|
||||
Space,
|
||||
Stack,
|
||||
Table,
|
||||
Text
|
||||
Table
|
||||
} from '@mantine/core';
|
||||
import type { ContextModalProps } from '@mantine/modals';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
@@ -20,11 +19,11 @@ import { useShallow } from 'zustand/react/shallow';
|
||||
import { api } from '../../App';
|
||||
import { generateUrl } from '../../functions/urls';
|
||||
import { useServerApiState } from '../../states/ServerApiState';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import { CopyButton } from '../buttons/CopyButton';
|
||||
import { StylishText } from '../items/StylishText';
|
||||
|
||||
import type { JSX } from 'react';
|
||||
import { OnlyStaff } from '../items/OnlyStaff';
|
||||
|
||||
type AboutLookupRef = {
|
||||
ref: string;
|
||||
@@ -42,15 +41,11 @@ export function AboutInvenTreeModal({
|
||||
modalBody: string;
|
||||
}>
|
||||
>) {
|
||||
const [user] = useUserState(useShallow((state) => [state.user]));
|
||||
|
||||
if (!user?.is_staff)
|
||||
return (
|
||||
<Text>
|
||||
<Trans>This information is only available for staff users</Trans>
|
||||
</Text>
|
||||
);
|
||||
return <AboutContent context={context} id={id} innerProps={innerProps} />;
|
||||
return (
|
||||
<OnlyStaff>
|
||||
<AboutContent context={context} id={id} innerProps={innerProps} />
|
||||
</OnlyStaff>
|
||||
);
|
||||
}
|
||||
|
||||
const AboutContent = ({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
ActionIcon,
|
||||
Alert,
|
||||
Container,
|
||||
Group,
|
||||
Indicator,
|
||||
@@ -39,7 +40,7 @@ import {
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import { ScanButton } from '../buttons/ScanButton';
|
||||
import { SpotlightButton } from '../buttons/SpotlightButton';
|
||||
import { Alerts } from './Alerts';
|
||||
import { Alerts, errorCodeLink } from './Alerts';
|
||||
import { MainMenu } from './MainMenu';
|
||||
import { NavHoverMenu } from './NavHoverMenu';
|
||||
import { NavigationDrawer } from './NavigationDrawer';
|
||||
@@ -53,11 +54,12 @@ export function Header() {
|
||||
const [server] = useServerApiState(useShallow((state) => [state.server]));
|
||||
const [navDrawerOpened, { open: openNavDrawer, close: closeNavDrawer }] =
|
||||
useDisclosure(navigationOpen);
|
||||
|
||||
const [
|
||||
searchDrawerOpened,
|
||||
{ open: openSearchDrawer, close: closeSearchDrawer }
|
||||
] = useDisclosure(false);
|
||||
const [elevatedAlertClosed, setElevatedAlertClosed] =
|
||||
useState<boolean>(false);
|
||||
|
||||
useHotkeys([
|
||||
[
|
||||
@@ -79,7 +81,7 @@ export function Header() {
|
||||
{ open: openNotificationDrawer, close: closeNotificationDrawer }
|
||||
] = useDisclosure(false);
|
||||
|
||||
const { isLoggedIn } = useUserState();
|
||||
const { isLoggedIn, user } = useUserState();
|
||||
const [notificationCount, setNotificationCount] = useState<number>(0);
|
||||
const globalSettings = useGlobalSettingsState();
|
||||
const userSettings = useUserSettingsState();
|
||||
@@ -128,6 +130,14 @@ export function Header() {
|
||||
else closeNavDrawer();
|
||||
}, [navigationOpen]);
|
||||
|
||||
const showElevated = useMemo(
|
||||
() =>
|
||||
(user?.is_staff || user?.is_superuser || false) &&
|
||||
!elevatedAlertClosed &&
|
||||
!window.INVENTREE_SETTINGS.dangerous_hide_evelevated_alert,
|
||||
[user, elevatedAlertClosed]
|
||||
);
|
||||
|
||||
const headerStyle: any = useMemo(() => {
|
||||
const sticky: boolean = userSettings.isSet('STICKY_HEADER', true);
|
||||
|
||||
@@ -200,6 +210,19 @@ export function Header() {
|
||||
</Group>
|
||||
</Group>
|
||||
</Container>
|
||||
{showElevated && user && (
|
||||
<Alert
|
||||
color={user.is_superuser ? 'red' : 'orange'}
|
||||
title={user.is_superuser ? t`Superuser Mode` : t`Administrator Mode`}
|
||||
withCloseButton
|
||||
onClose={() => setElevatedAlertClosed(true)}
|
||||
>
|
||||
<Text>
|
||||
{t`The current user has elevated privileges and should not be used for regular usage.`}
|
||||
{errorCodeLink('INVE-W14')}
|
||||
</Text>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ declare global {
|
||||
sentry_dsn?: string;
|
||||
environment?: string;
|
||||
mobile_mode?: 'default' | 'allow-ignore' | 'allow-always';
|
||||
dangerous_hide_evelevated_alert?: boolean;
|
||||
};
|
||||
react: typeof React;
|
||||
React: typeof React;
|
||||
|
||||
@@ -69,7 +69,7 @@ export function AccountDetailPanel() {
|
||||
value: <YesNoUndefinedButton value={user?.profile?.active} />
|
||||
},
|
||||
{
|
||||
label: t`Staff Access`,
|
||||
label: t`Administrator`,
|
||||
value: <YesNoUndefinedButton value={user?.is_staff} />
|
||||
},
|
||||
{
|
||||
|
||||
@@ -84,14 +84,14 @@ export default function UserDetail() {
|
||||
{
|
||||
type: 'boolean',
|
||||
name: 'is_staff',
|
||||
label: t`Staff`,
|
||||
icon: 'info'
|
||||
label: t`Administrator`,
|
||||
icon: 'warning'
|
||||
},
|
||||
{
|
||||
type: 'boolean',
|
||||
name: 'is_superuser',
|
||||
label: t`Superuser`,
|
||||
icon: 'info'
|
||||
icon: 'warning'
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
@@ -197,13 +197,13 @@ export default function UserDetail() {
|
||||
? []
|
||||
: [
|
||||
instance.is_staff && (
|
||||
<Badge key='is_staff' color='blue'>{t`Staff`}</Badge>
|
||||
<Badge key='is_staff' color='orange'>{t`Administrator`}</Badge>
|
||||
),
|
||||
instance.is_superuser && (
|
||||
<Badge key='is_superuser' color='red'>{t`Superuser`}</Badge>
|
||||
),
|
||||
!instance.is_staff && !instance.is_superuser && (
|
||||
<Badge key='is_normal' color='yellow'>{t`Basic user`}</Badge>
|
||||
<Badge key='is_normal' color='yellow'>{t`Normal user`}</Badge>
|
||||
),
|
||||
instance.is_active ? (
|
||||
<Badge key='is_active' color='green'>{t`Active`}</Badge>
|
||||
|
||||
@@ -180,7 +180,7 @@ export function UserDrawer({
|
||||
disabled: isCurrentUser
|
||||
},
|
||||
is_staff: {
|
||||
label: t`Is Staff`,
|
||||
label: t`Is Administrator`,
|
||||
description: t`Designates whether the user can log into the django admin site.`,
|
||||
disabled: isCurrentUser
|
||||
},
|
||||
@@ -281,7 +281,8 @@ export function UserTable({
|
||||
}
|
||||
},
|
||||
BooleanColumn({
|
||||
accessor: 'is_staff'
|
||||
accessor: 'is_staff',
|
||||
title: t`Administrator`
|
||||
}),
|
||||
BooleanColumn({
|
||||
accessor: 'is_superuser'
|
||||
@@ -402,8 +403,8 @@ export function UserTable({
|
||||
},
|
||||
{
|
||||
name: 'is_staff',
|
||||
label: t`Staff`,
|
||||
description: t`Show staff users`
|
||||
label: t`Administrator`,
|
||||
description: t`Show administrators`
|
||||
},
|
||||
{
|
||||
name: 'is_superuser',
|
||||
|
||||
Reference in New Issue
Block a user