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 Illuminate\Http\Request;
use Pterodactyl\Models\Server; use Pterodactyl\Models\Server;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Pterodactyl\Models\Permission;
use Pterodactyl\Repositories\Eloquent\SubuserRepository; use Pterodactyl\Repositories\Eloquent\SubuserRepository;
use Pterodactyl\Services\Subusers\SubuserCreationService; use Pterodactyl\Services\Subusers\SubuserCreationService;
use Pterodactyl\Transformers\Api\Client\SubuserTransformer; use Pterodactyl\Transformers\Api\Client\SubuserTransformer;
@ -123,6 +124,6 @@ class SubuserController extends ClientApiController
*/ */
protected function getDefaultPermissions(Request $request): array 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 { Subuser } from '@/state/server/subusers';
import { Form, Formik, FormikHelpers, useFormikContext } from 'formik'; import { Form, Formik, FormikHelpers, useFormikContext } from 'formik';
import { array, object, string } from 'yup'; import { array, object, string } from 'yup';
@ -16,6 +16,7 @@ import { httpErrorToHuman } from '@/api/http';
import FlashMessageRender from '@/components/FlashMessageRender'; import FlashMessageRender from '@/components/FlashMessageRender';
import Can from '@/components/elements/Can'; import Can from '@/components/elements/Can';
import { usePermissions } from '@/plugins/usePermissions'; import { usePermissions } from '@/plugins/usePermissions';
import { useDeepMemo } from '@/plugins/useDeepMemo';
type Props = { type Props = {
subuser?: Subuser; subuser?: Subuser;
@ -37,12 +38,38 @@ const PermissionLabel = styled.label`
${tw`border-neutral-500 bg-neutral-800`}; ${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 EditSubuserModal = forwardRef<HTMLHeadingElement, Props>(({ subuser, ...props }, ref) => {
const { values, isSubmitting, setFieldValue } = useFormikContext<Values>(); const { values, isSubmitting, setFieldValue } = useFormikContext<Values>();
const [ canEditUser ] = usePermissions([ 'user.update' ]); const [ canEditUser ] = usePermissions(subuser ? [ 'user.update' ] : [ 'user.create' ]);
const permissions = useStoreState((state: ApplicationStore) => state.permissions.data); 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 ( return (
<Modal {...props} top={false} showSpinnerOverlay={isSubmitting}> <Modal {...props} top={false} showSpinnerOverlay={isSubmitting}>
@ -54,6 +81,12 @@ const EditSubuserModal = forwardRef<HTMLHeadingElement, Props>(({ subuser, ...pr
} }
</h3> </h3>
<FlashMessageRender byKey={'user:edit'} className={'mt-4'}/> <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 && {!subuser &&
<div className={'mt-6'}> <div className={'mt-6'}>
<Field <Field
@ -70,7 +103,7 @@ const EditSubuserModal = forwardRef<HTMLHeadingElement, Props>(({ subuser, ...pr
title={ title={
<div className={'flex items-center'}> <div className={'flex items-center'}>
<p className={'text-sm uppercase flex-1'}>{key}</p> <p className={'text-sm uppercase flex-1'}>{key}</p>
{canEditUser && {canEditUser && editablePermissions.indexOf(key) >= 0 &&
<input <input
type={'checkbox'} type={'checkbox'}
onClick={e => { onClick={e => {
@ -106,7 +139,7 @@ const EditSubuserModal = forwardRef<HTMLHeadingElement, Props>(({ subuser, ...pr
htmlFor={`permission_${key}_${pkey}`} htmlFor={`permission_${key}_${pkey}`}
className={classNames('transition-colors duration-75', { className={classNames('transition-colors duration-75', {
'mt-2': index !== 0, 'mt-2': index !== 0,
disabled: !canEditUser, disabled: !canEditUser || editablePermissions.indexOf(`${key}.${pkey}`) < 0,
})} })}
> >
<div className={'p-2'}> <div className={'p-2'}>
@ -115,7 +148,7 @@ const EditSubuserModal = forwardRef<HTMLHeadingElement, Props>(({ subuser, ...pr
name={'permissions'} name={'permissions'}
value={`${key}.${pkey}`} value={`${key}.${pkey}`}
className={'w-5 h-5 mr-2'} className={'w-5 h-5 mr-2'}
disabled={!canEditUser} disabled={!canEditUser || editablePermissions.indexOf(`${key}.${pkey}`) < 0}
/> />
</div> </div>
<div className={'flex-1'}> <div className={'flex-1'}>
@ -133,7 +166,7 @@ const EditSubuserModal = forwardRef<HTMLHeadingElement, Props>(({ subuser, ...pr
</TitledGreyBox> </TitledGreyBox>
))} ))}
</div> </div>
<Can action={subuser ? 'user.update' : 'user.delete'}> <Can action={subuser ? 'user.update' : 'user.create'}>
<div className={'pb-6 flex justify-end'}> <div className={'pb-6 flex justify-end'}>
<button className={'btn btn-primary btn-sm'} type={'submit'}> <button className={'btn btn-primary btn-sm'} type={'submit'}>
{subuser ? 'Save' : 'Invite User'} {subuser ? 'Save' : 'Invite User'}
@ -169,6 +202,10 @@ export default ({ subuser, ...props }: Props) => {
}); });
}; };
useEffect(() => {
clearFlashes('user:edit');
}, []);
return ( return (
<Formik <Formik
onSubmit={submit} 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 { faUserLock } from '@fortawesome/free-solid-svg-icons/faUserLock';
import classNames from 'classnames'; import classNames from 'classnames';
import Can from '@/components/elements/Can'; import Can from '@/components/elements/Can';
import { useStoreState } from 'easy-peasy';
interface Props { interface Props {
subuser: Subuser; subuser: Subuser;
} }
export default ({ subuser }: Props) => { export default ({ subuser }: Props) => {
const uuid = useStoreState(state => state.user!.data!.uuid);
const [ visible, setVisible ] = useState(false); const [ visible, setVisible ] = useState(false);
return ( return (
@ -54,7 +56,9 @@ export default ({ subuser }: Props) => {
<button <button
type={'button'} type={'button'}
aria-label={'Edit subuser'} 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)} onClick={() => setVisible(true)}
> >
<FontAwesomeIcon icon={faPencilAlt}/> <FontAwesomeIcon icon={faPencilAlt}/>

View file

@ -229,7 +229,7 @@ a.btn {
} }
input[type="checkbox"], input[type="radio"] { 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; color-adjust: exact;
background-origin: border-box; background-origin: border-box;
transition: all 75ms linear, box-shadow 25ms linear; transition: all 75ms linear, box-shadow 25ms linear;