Update frontend to only allow selection of valid permissions for subusers

This commit is contained in:
Dane Everitt 2020-04-19 11:58:26 -07:00
parent 00b0d30c60
commit a1c3730861
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
4 changed files with 52 additions and 10 deletions

View file

@ -5,6 +5,7 @@ namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
use Illuminate\Http\Request;
use Pterodactyl\Models\Server;
use Illuminate\Http\JsonResponse;
use Pterodactyl\Models\Permission;
use Pterodactyl\Repositories\Eloquent\SubuserRepository;
use Pterodactyl\Services\Subusers\SubuserCreationService;
use Pterodactyl\Transformers\Api\Client\SubuserTransformer;
@ -123,6 +124,6 @@ class SubuserController extends ClientApiController
*/
protected function getDefaultPermissions(Request $request): array
{
return array_unique(array_merge($request->input('permissions') ?? [], ['websocket.connect']));
return array_unique(array_merge($request->input('permissions') ?? [], [Permission::ACTION_WEBSOCKET_CONNECT]));
}
}

View file

@ -1,4 +1,4 @@
import React, { forwardRef, useRef } from 'react';
import React, { forwardRef, useEffect, useRef } from 'react';
import { Subuser } from '@/state/server/subusers';
import { Form, Formik, FormikHelpers, useFormikContext } from 'formik';
import { array, object, string } from 'yup';
@ -16,6 +16,7 @@ import { httpErrorToHuman } from '@/api/http';
import FlashMessageRender from '@/components/FlashMessageRender';
import Can from '@/components/elements/Can';
import { usePermissions } from '@/plugins/usePermissions';
import { useDeepMemo } from '@/plugins/useDeepMemo';
type Props = {
subuser?: Subuser;
@ -37,12 +38,38 @@ const PermissionLabel = styled.label`
${tw`border-neutral-500 bg-neutral-800`};
}
}
&.disabled {
${tw`opacity-50`};
& input[type="checkbox"]:not(:checked) {
${tw`border-0`};
}
}
`;
const EditSubuserModal = forwardRef<HTMLHeadingElement, Props>(({ subuser, ...props }, ref) => {
const { values, isSubmitting, setFieldValue } = useFormikContext<Values>();
const [ canEditUser ] = usePermissions([ 'user.update' ]);
const permissions = useStoreState((state: ApplicationStore) => state.permissions.data);
const [ canEditUser ] = usePermissions(subuser ? [ 'user.update' ] : [ 'user.create' ]);
const permissions = useStoreState(state => state.permissions.data);
// The currently logged in user's permissions. We're going to filter out any permissions
// that they should not need.
const loggedInPermissions = ServerContext.useStoreState(state => state.server.permissions);
// The permissions that can be modified by this user.
const editablePermissions = useDeepMemo(() => {
const cleaned = Object.keys(permissions)
.map(key => Object.keys(permissions[key].keys).map(pkey => `${key}.${pkey}`));
const list: string[] = ([] as string[]).concat.apply([], Object.values(cleaned));
if (loggedInPermissions.length === 1 && loggedInPermissions[0] === '*') {
return list;
}
return list.filter(key => loggedInPermissions.indexOf(key) >= 0);
}, [permissions, loggedInPermissions]);
return (
<Modal {...props} top={false} showSpinnerOverlay={isSubmitting}>
@ -54,6 +81,12 @@ const EditSubuserModal = forwardRef<HTMLHeadingElement, Props>(({ subuser, ...pr
}
</h3>
<FlashMessageRender byKey={'user:edit'} className={'mt-4'}/>
<div className={'mt-4 pl-4 py-2 border-l-4 border-cyan-400'}>
<p className={'text-sm text-neutral-300'}>
Only permissions which your account is currently assigned may be selected when creating or
modifying other users.
</p>
</div>
{!subuser &&
<div className={'mt-6'}>
<Field
@ -70,7 +103,7 @@ const EditSubuserModal = forwardRef<HTMLHeadingElement, Props>(({ subuser, ...pr
title={
<div className={'flex items-center'}>
<p className={'text-sm uppercase flex-1'}>{key}</p>
{canEditUser &&
{canEditUser && editablePermissions.indexOf(key) >= 0 &&
<input
type={'checkbox'}
onClick={e => {
@ -106,7 +139,7 @@ const EditSubuserModal = forwardRef<HTMLHeadingElement, Props>(({ subuser, ...pr
htmlFor={`permission_${key}_${pkey}`}
className={classNames('transition-colors duration-75', {
'mt-2': index !== 0,
disabled: !canEditUser,
disabled: !canEditUser || editablePermissions.indexOf(`${key}.${pkey}`) < 0,
})}
>
<div className={'p-2'}>
@ -115,7 +148,7 @@ const EditSubuserModal = forwardRef<HTMLHeadingElement, Props>(({ subuser, ...pr
name={'permissions'}
value={`${key}.${pkey}`}
className={'w-5 h-5 mr-2'}
disabled={!canEditUser}
disabled={!canEditUser || editablePermissions.indexOf(`${key}.${pkey}`) < 0}
/>
</div>
<div className={'flex-1'}>
@ -133,7 +166,7 @@ const EditSubuserModal = forwardRef<HTMLHeadingElement, Props>(({ subuser, ...pr
</TitledGreyBox>
))}
</div>
<Can action={subuser ? 'user.update' : 'user.delete'}>
<Can action={subuser ? 'user.update' : 'user.create'}>
<div className={'pb-6 flex justify-end'}>
<button className={'btn btn-primary btn-sm'} type={'submit'}>
{subuser ? 'Save' : 'Invite User'}
@ -169,6 +202,10 @@ export default ({ subuser, ...props }: Props) => {
});
};
useEffect(() => {
clearFlashes('user:edit');
}, []);
return (
<Formik
onSubmit={submit}

View file

@ -8,12 +8,14 @@ import { faUnlockAlt } from '@fortawesome/free-solid-svg-icons/faUnlockAlt';
import { faUserLock } from '@fortawesome/free-solid-svg-icons/faUserLock';
import classNames from 'classnames';
import Can from '@/components/elements/Can';
import { useStoreState } from 'easy-peasy';
interface Props {
subuser: Subuser;
}
export default ({ subuser }: Props) => {
const uuid = useStoreState(state => state.user!.data!.uuid);
const [ visible, setVisible ] = useState(false);
return (
@ -54,7 +56,9 @@ export default ({ subuser }: Props) => {
<button
type={'button'}
aria-label={'Edit subuser'}
className={'block text-sm p-2 text-neutral-500 hover:text-neutral-100 transition-colors duration-150 mx-4'}
className={classNames('block text-sm p-2 text-neutral-500 hover:text-neutral-100 transition-colors duration-150 mx-4', {
hidden: subuser.uuid === uuid,
})}
onClick={() => setVisible(true)}
>
<FontAwesomeIcon icon={faPencilAlt}/>

View file

@ -229,7 +229,7 @@ a.btn {
}
input[type="checkbox"], input[type="radio"] {
@apply .appearance-none .inline-block .align-middle .select-none .flex-no-shrink .w-4 .h-4 .text-primary-400 .border .border-neutral-300 .rounded-sm;
@apply .cursor-pointer .appearance-none .inline-block .align-middle .select-none .flex-no-shrink .w-4 .h-4 .text-primary-400 .border .border-neutral-300 .rounded-sm;
color-adjust: exact;
background-origin: border-box;
transition: all 75ms linear, box-shadow 25ms linear;