fix: do not allow editing of user settings under certain conditions (#1168)

* fix: do not allow editing of user settings under certain conditions

* feat(lang): generate translation keys for new strings

* refactor: modify owner check for clarity

* fix(ui): hide buttons where appropriate and add missing translation string
pull/1161/head
TheCatLady 3 years ago committed by GitHub
parent 0bd0912613
commit 001dcd328c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -73,6 +73,14 @@ userSettingsRoutes.post<
return next({ status: 404, message: 'User not found.' }); return next({ status: 404, message: 'User not found.' });
} }
// "Owner" user settings cannot be modified by other users
if (user.id === 1 && req.user?.id !== 1) {
return next({
status: 403,
message: "You do not have permission to modify this user's settings.",
});
}
user.username = req.body.username; user.username = req.body.username;
if (!user.settings) { if (!user.settings) {
user.settings = new UserSettings({ user.settings = new UserSettings({
@ -240,6 +248,14 @@ userSettingsRoutes.post<
return next({ status: 404, message: 'User not found.' }); return next({ status: 404, message: 'User not found.' });
} }
// "Owner" user settings cannot be modified by other users
if (user.id === 1 && req.user?.id !== 1) {
return next({
status: 403,
message: "You do not have permission to modify this user's settings.",
});
}
if (!user.settings) { if (!user.settings) {
user.settings = new UserSettings({ user.settings = new UserSettings({
user: req.user, user: req.user,
@ -309,8 +325,8 @@ userSettingsRoutes.post<
return next({ status: 404, message: 'User not found.' }); return next({ status: 404, message: 'User not found.' });
} }
// Only let the owner user modify themselves // "Owner" user permissions cannot be modified, and users cannot set their own permissions
if (user.id === 1 && req.user?.id !== 1) { if (user.id === 1 || req.user?.id === user.id) {
return next({ return next({
status: 403, status: 403,
message: 'You do not have permission to modify this user', message: 'You do not have permission to modify this user',

@ -559,6 +559,7 @@ const UserList: React.FC = () => {
<Table.TD alignText="right"> <Table.TD alignText="right">
<Button <Button
buttonType="warning" buttonType="warning"
disabled={user.id === 1 && currentUser?.id !== 1}
className="mr-2" className="mr-2"
onClick={() => onClick={() =>
router.push( router.push(
@ -571,7 +572,11 @@ const UserList: React.FC = () => {
</Button> </Button>
<Button <Button
buttonType="danger" buttonType="danger"
disabled={hasPermission(Permission.ADMIN, user.permissions)} disabled={
user.id === 1 ||
(currentUser?.id !== 1 &&
hasPermission(Permission.ADMIN, user.permissions))
}
onClick={() => setDeleteModal({ isOpen: true, user })} onClick={() => setDeleteModal({ isOpen: true, user })}
> >
{intl.formatMessage(messages.delete)} {intl.formatMessage(messages.delete)}

@ -82,7 +82,7 @@ const ProfileHeader: React.FC<ProfileHeaderProps> = ({
</div> </div>
<div className="flex flex-col-reverse mt-6 space-y-4 space-y-reverse justify-stretch sm:flex-row-reverse sm:justify-end sm:space-x-reverse sm:space-y-0 sm:space-x-3 md:mt-0 md:flex-row md:space-x-3"> <div className="flex flex-col-reverse mt-6 space-y-4 space-y-reverse justify-stretch sm:flex-row-reverse sm:justify-end sm:space-x-reverse sm:space-y-0 sm:space-x-3 md:mt-0 md:flex-row md:space-x-3">
{(loggedInUser?.id === user.id || {(loggedInUser?.id === user.id ||
hasPermission(Permission.MANAGE_USERS)) && (user.id !== 1 && hasPermission(Permission.MANAGE_USERS))) &&
!isSettingsPage ? ( !isSettingsPage ? (
<Link <Link
href={ href={
@ -109,28 +109,30 @@ const ProfileHeader: React.FC<ProfileHeaderProps> = ({
</Button> </Button>
</Link> </Link>
) : ( ) : (
<Link isSettingsPage && (
href={ <Link
loggedInUser?.id === user.id ? `/profile` : `/users/${user.id}` href={
} loggedInUser?.id === user.id ? `/profile` : `/users/${user.id}`
passHref }
> passHref
<Button as="a"> >
<svg <Button as="a">
className="w-5 h-5 mr-1" <svg
fill="currentColor" className="w-5 h-5 mr-1"
viewBox="0 0 20 20" fill="currentColor"
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
> xmlns="http://www.w3.org/2000/svg"
<path >
fillRule="evenodd" <path
d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" fillRule="evenodd"
clipRule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z"
/> clipRule="evenodd"
</svg> />
{intl.formatMessage(messages.profile)} </svg>
</Button> {intl.formatMessage(messages.profile)}
</Link> </Button>
</Link>
)
)} )}
</div> </div>
</div> </div>

@ -10,6 +10,7 @@ import Error from '../../../../pages/_error';
import Button from '../../../Common/Button'; import Button from '../../../Common/Button';
import LoadingSpinner from '../../../Common/LoadingSpinner'; import LoadingSpinner from '../../../Common/LoadingSpinner';
import PermissionEdit from '../../../PermissionEdit'; import PermissionEdit from '../../../PermissionEdit';
import Alert from '../../../Common/Alert';
const messages = defineMessages({ const messages = defineMessages({
displayName: 'Display Name', displayName: 'Display Name',
@ -20,6 +21,8 @@ const messages = defineMessages({
toastSettingsSuccess: 'Settings successfully saved!', toastSettingsSuccess: 'Settings successfully saved!',
toastSettingsFailure: 'Something went wrong while saving settings.', toastSettingsFailure: 'Something went wrong while saving settings.',
permissions: 'Permissions', permissions: 'Permissions',
unauthorized: 'Unauthorized',
unauthorizedDescription: 'You cannot modify your own permissions.',
}); });
const UserPermissions: React.FC = () => { const UserPermissions: React.FC = () => {
@ -40,6 +43,21 @@ const UserPermissions: React.FC = () => {
return <Error statusCode={500} />; return <Error statusCode={500} />;
} }
if (currentUser?.id !== 1 && currentUser?.id === user?.id) {
return (
<>
<div className="mb-6">
<h3 className="heading">
{intl.formatMessage(messages.permissions)}
</h3>
</div>
<Alert title={intl.formatMessage(messages.unauthorized)} type="error">
{intl.formatMessage(messages.unauthorizedDescription)}
</Alert>
</>
);
}
return ( return (
<> <>
<div className="mb-6"> <div className="mb-6">

@ -9,6 +9,7 @@ import LoadingSpinner from '../../Common/LoadingSpinner';
import PageTitle from '../../Common/PageTitle'; import PageTitle from '../../Common/PageTitle';
import ProfileHeader from '../ProfileHeader'; import ProfileHeader from '../ProfileHeader';
import useSettings from '../../../hooks/useSettings'; import useSettings from '../../../hooks/useSettings';
import Alert from '../../Common/Alert';
const messages = defineMessages({ const messages = defineMessages({
settings: 'User Settings', settings: 'User Settings',
@ -16,6 +17,9 @@ const messages = defineMessages({
menuChangePass: 'Password', menuChangePass: 'Password',
menuNotifications: 'Notifications', menuNotifications: 'Notifications',
menuPermissions: 'Permissions', menuPermissions: 'Permissions',
unauthorized: 'Unauthorized',
unauthorizedDescription:
"You do not have permission to modify this user's settings.",
}); });
interface SettingsRoute { interface SettingsRoute {
@ -24,6 +28,7 @@ interface SettingsRoute {
regex: RegExp; regex: RegExp;
requiredPermission?: Permission | Permission[]; requiredPermission?: Permission | Permission[];
permissionType?: { type: 'and' | 'or' }; permissionType?: { type: 'and' | 'or' };
hidden?: boolean;
} }
const UserSettings: React.FC = ({ children }) => { const UserSettings: React.FC = ({ children }) => {
@ -51,6 +56,15 @@ const UserSettings: React.FC = ({ children }) => {
text: intl.formatMessage(messages.menuChangePass), text: intl.formatMessage(messages.menuChangePass),
route: '/settings/password', route: '/settings/password',
regex: /\/settings\/password/, regex: /\/settings\/password/,
hidden:
(!settings.currentSettings.localLogin &&
!hasPermission(
Permission.MANAGE_SETTINGS,
currentUser?.permissions ?? 0
)) ||
(currentUser?.id !== 1 &&
currentUser?.id !== user?.id &&
hasPermission(Permission.ADMIN, user?.permissions ?? 0)),
}, },
{ {
text: intl.formatMessage(messages.menuNotifications), text: intl.formatMessage(messages.menuNotifications),
@ -62,6 +76,7 @@ const UserSettings: React.FC = ({ children }) => {
route: '/settings/permissions', route: '/settings/permissions',
regex: /\/settings\/permissions/, regex: /\/settings\/permissions/,
requiredPermission: Permission.MANAGE_USERS, requiredPermission: Permission.MANAGE_USERS,
hidden: currentUser?.id !== 1 && currentUser?.id === user.id,
}, },
]; ];
@ -76,20 +91,6 @@ const UserSettings: React.FC = ({ children }) => {
regex: RegExp; regex: RegExp;
isMobile?: boolean; isMobile?: boolean;
}> = ({ children, route, regex, isMobile = false }) => { }> = ({ children, route, regex, isMobile = false }) => {
if (
route === '/settings/password' &&
((!settings.currentSettings.localLogin &&
!hasPermission(
Permission.MANAGE_SETTINGS,
currentUser?.permissions ?? 0
)) ||
(currentUser?.id !== 1 &&
currentUser?.id !== user?.id &&
hasPermission(Permission.ADMIN, user?.permissions ?? 0)))
) {
return null;
}
const finalRoute = router.asPath.includes('/profile') const finalRoute = router.asPath.includes('/profile')
? `/profile${route}` ? `/profile${route}`
: `/users/${user.id}${route}`; : `/users/${user.id}${route}`;
@ -111,6 +112,20 @@ const UserSettings: React.FC = ({ children }) => {
); );
}; };
if (currentUser?.id !== 1 && user.id === 1) {
return (
<>
<PageTitle title={intl.formatMessage(messages.settings)} />
<ProfileHeader user={user} isSettingsPage />
<div className="mt-6">
<Alert title={intl.formatMessage(messages.unauthorized)} type="error">
{intl.formatMessage(messages.unauthorizedDescription)}
</Alert>
</div>
</>
);
}
const currentRoute = settingsRoutes.find( const currentRoute = settingsRoutes.find(
(route) => !!router.pathname.match(route.regex) (route) => !!router.pathname.match(route.regex)
)?.route; )?.route;
@ -136,14 +151,16 @@ const UserSettings: React.FC = ({ children }) => {
aria-label="Selected tab" aria-label="Selected tab"
> >
{settingsRoutes {settingsRoutes
.filter((route) => .filter(
route.requiredPermission (route) =>
? hasPermission( !route.hidden &&
route.requiredPermission, (route.requiredPermission
currentUser?.permissions ?? 0, ? hasPermission(
route.permissionType route.requiredPermission,
) currentUser?.permissions ?? 0,
: true route.permissionType
)
: true)
) )
.map((route, index) => ( .map((route, index) => (
<SettingsLink <SettingsLink
@ -161,14 +178,16 @@ const UserSettings: React.FC = ({ children }) => {
<div className="border-b border-gray-600"> <div className="border-b border-gray-600">
<nav className="flex -mb-px"> <nav className="flex -mb-px">
{settingsRoutes {settingsRoutes
.filter((route) => .filter(
route.requiredPermission (route) =>
? hasPermission( !route.hidden &&
route.requiredPermission, (route.requiredPermission
currentUser?.permissions ?? 0, ? hasPermission(
route.permissionType route.requiredPermission,
) currentUser?.permissions ?? 0,
: true route.permissionType
)
: true)
) )
.map((route, index) => ( .map((route, index) => (
<SettingsLink <SettingsLink

@ -16,6 +16,7 @@ import { defineMessages, useIntl } from 'react-intl';
const messages = defineMessages({ const messages = defineMessages({
recentrequests: 'Recent Requests', recentrequests: 'Recent Requests',
norequests: 'No Requests',
}); });
type MediaTitle = MovieDetails | TvDetails; type MediaTitle = MovieDetails | TvDetails;
@ -95,7 +96,7 @@ const UserProfile: React.FC = () => {
/> />
))} ))}
placeholder={<RequestCard.Placeholder />} placeholder={<RequestCard.Placeholder />}
emptyMessage={'No Requests'} emptyMessage={intl.formatMessage(messages.norequests)}
/> />
</div> </div>
</> </>

@ -773,11 +773,15 @@
"components.UserProfile.UserSettings.UserPermissions.saving": "Saving…", "components.UserProfile.UserSettings.UserPermissions.saving": "Saving…",
"components.UserProfile.UserSettings.UserPermissions.toastSettingsFailure": "Something went wrong while saving settings.", "components.UserProfile.UserSettings.UserPermissions.toastSettingsFailure": "Something went wrong while saving settings.",
"components.UserProfile.UserSettings.UserPermissions.toastSettingsSuccess": "Settings successfully saved!", "components.UserProfile.UserSettings.UserPermissions.toastSettingsSuccess": "Settings successfully saved!",
"components.UserProfile.UserSettings.UserPermissions.unauthorized": "Unauthorized",
"components.UserProfile.UserSettings.UserPermissions.unauthorizedDescription": "You cannot modify your own permissions.",
"components.UserProfile.UserSettings.menuChangePass": "Password", "components.UserProfile.UserSettings.menuChangePass": "Password",
"components.UserProfile.UserSettings.menuGeneralSettings": "General Settings", "components.UserProfile.UserSettings.menuGeneralSettings": "General Settings",
"components.UserProfile.UserSettings.menuNotifications": "Notifications", "components.UserProfile.UserSettings.menuNotifications": "Notifications",
"components.UserProfile.UserSettings.menuPermissions": "Permissions", "components.UserProfile.UserSettings.menuPermissions": "Permissions",
"components.UserProfile.UserSettings.settings": "User Settings", "components.UserProfile.UserSettings.settings": "User Settings",
"components.UserProfile.UserSettings.unauthorized": "Unauthorized",
"components.UserProfile.UserSettings.unauthorizedDescription": "You do not have permission to modify this user's settings.",
"components.UserProfile.recentrequests": "Recent Requests", "components.UserProfile.recentrequests": "Recent Requests",
"i18n.advanced": "Advanced", "i18n.advanced": "Advanced",
"i18n.approve": "Approve", "i18n.approve": "Approve",

Loading…
Cancel
Save